Skip to main content

Full text of "develop - Issue 19 - September 1994"

See other formats


Pick Your Picker With Color Picker 2.0 © Media Capture Using the Sequence Grabber 


Tuning PowerPC 
Memory Usage 


Designing for the 
Power Macintosh 


Adding QuickDraw 
GX Printing to 
QuickDraw 
Applications 


Making the Most 
of QuickDraw GX 
Bitmaps 


Inheritance in 
Scripts 


$10.00 


4 


Issue 19 September 1994 


Sears 


s 
se aa 
a 
ee 
a 
es 
os 


develop 


EDITORIAL STAFF 


Editor-in-Cheek Caroline Rose 

Managing Editor Cynthia Jasper 

Technical Buckstopper Dave Johnson 

Bookmark CD Leader Alex Dosher 

Our Boss Greg Joswiak 

His Boss Dennis Matthews 

Review Board Pete (“Luke”) Alexander, Dave 
Radcliffe, Fim Reekes, Bryan K. (“Beaker”) 
Ressler, Larry Rosenstein, Andy Shebanow, 
Gregg Williams 

Contributing Editors Lorraine Anderson, 
Steve Chernicoff, Toni Haskell, Judy 
Helfand, Jody Larson, Foe Williams 


Indexer Marc Savage 


ART & PRODUCTION 


Production/Art Director Diane Wilcox 


Technical Illustration Ruth Anderson, 
Sandee Karr 


Formatting Forbes Mill Press 

Film Services Aptos Post, Inc. 

Prepress Production PrePress Assembly 

Printing Wolfer Printing Company, Inc. 

Photography Sharon Beals, Cynthia Jasper, 
Mark Maxham, John Wang, Diane Wilcox 


Cover Illustration Hal Rucker and Peter 
Andrea of Rucker Huggins Design 


ISSN #1047-0735. © 1994 Apple Computer, 
Inc. All rights reserved. Apple, the Apple logo, 
APDA, AppleLink, AppleTalk, HyperCard, 
ImageWriter, LaserWriter, Mac, MacApp, 
Macintosh, MacTCP, MPW, Newton, 
QuickTime, and TrueType are trademarks of 
Apple Computer, Inc., registered in the U.S. and 
other countries. AOCE, AppleScript, A/ROSE, 
Balloon Help, ColorSync, develop, DocViewer, 
Finder, MessagePad, NewtonMail, NewtonScript, 
OpenDoc, Power Macintosh, PowerShare, 
PowerTalk, QuickDraw, and QuickTake are 
trademarks of Apple Computer, Inc. PostScript is 
a trademark of Adobe Systems Incorporated, 
which may be registered in certain jurisdictions. 
PowerPC is a trademark of International Business 
Machines Corporation, used under license 
therefrom. FaceSpan is a trademark of Software 
Designs Unlimited, Inc. All other trademarks are 
the property of their respective owners. 


& Printed on recycled paper 


THINGS TO KNOW 


develop, The Apple Technical 
Journal, a quarterly publication of 
Apple Computer’s Developer Press 
group, is published in March, June, 
September, and December. develop 
articles and code have been reviewed 
for robustness by Apple engineers. 


This issue’s CD. Subscription issues 
of develop are accompanied by the 
develop Bookmark CD. The Bookmark 
CD contains a subset of the materials 
on the monthly Developer CD Series, 
which is available from APDA. 
Included on the CD are this issue and 
all back issues of develop along with the 
code that the articles describe. The 
develop code is updated when necessary, 
so always use the most recent CD. 
The CD also contains Technical 
Notes, sample code, and other useful 
documentation and tools (these 
contents are subject to change). 
Software and documentation referred 
to as being on this issue’s CD are 
located on either the Bookmark CD or 
the Reference Library or Tool Chest 
edition of the Developer CD Series. 


The develop issues and code are also 
available on AppleLink and via 
anonymous ftp at ftp.apple.com. 


Macintosh Technical Notes. 
Where references to Macintosh 
‘Technical Notes in develop are followed 
by something in parentheses like 
“(Memory 13),” this indicates the 
category and number of the Note on 
this issue’s CD. 


E-mail addresses. Most e-mail 
addresses mentioned in develop are 
AppleLink addresses; to convert one of 
these to an Internet address, append 
“@applelink.apple.com” to it. For 
example, DEVELOP on AppleLink 
becomes develop@applelink.apple.com 
on the Internet. To convert a 
NewtonMail address to an Internet 
address, append “@online.apple.com” 
to it. 


CONTACTING US 


Feedback. Send editorial suggestions 
or comments to Caroline Rose at 
AppleLink CROSE, Internet 
crose@applelink.apple.com, or fax 
(408)974-6395. Send technical 
questions about develop to Dave 
Johnson at AppleLink JOHNSON.DK, 
Internet dkj@apple.com, CompuServe 
75300,715, or fax (408)974-6395. Or 
write to Caroline or Dave at Apple 
Computer, Inc., One Infinite Loop, 
M/S 303-4DP, Cupertino, CA 95014. 


Article submissions. Ask for our 
Author’s Guidelines and a submission 
form at AppleLink DEVELOP, 
Internet develop@applelink.apple.com, 
or fax (408)974-6395. Or write to 
Caroline Rose at the above address. 


Subscriptions. Subscribe to develop 
through APDA (see below) or use the 
subscription card in this issue. For 
subscription changes or queries, call 
1-800-877-5548 in the U.S. or 
(815)734-1116 outside the U.S., or 
write to AppleLink DEV.SUBS, 
Internet dev.subs@applelink.apple.com, 
or develop, P.O. Box 531, Mount Morris, 
IL 61054-7858. 


Back issues. Printed back issues are 
available for $13 each in the U.S. or 
$20 outside the U.S. To order, call 
1-800-877-5548 in the U.S. or 
(815)734-1116 outside the U.S., or 
write to AppleLink DEV.SUBS, 
Internet dev.subs@applelink.apple.com, 
or develop, P.O. Box 531, Mount Morris, 
IL 61054-7858. 


APDA. To order products from APDA 
or receive a catalog, call 1-800-282- 
2732 in the U.S., 1-800-637-0029 in 
Canada, (716)871-6555 internationally, 
or (716)871-6511 for fax. Order 
electronically at AppleLink APDA, 
Internet apda@applelink.apple.com, 
CompuServe 76666,2405, or America 
Online APDAorder. Or write APDA, 
Apple Computer, Inc., P.O. Box 319, 
Buffalo, NY 14207-0319. 


24 


48 


68 


89 


17 


20 


65 


85 


Issue 19 September 1994 


ARTICLES 
Building an OpenDoc Part Handler by Kurt Piersol 


Writing code to support Apple’s new compound-document architecture is a lot like writing any Macintosh 
application. Here’s an overview of what you'll need to know. 


Adding QuickDraw GX Printing to QuickDraw Applications by Dave Hersey 
Even if your application doesn’t need the advanced graphics capabilities of QuickDraw GX, your users will 
love the new printing architecture, and you can support it with a minimum of effort. 


Making the Most of QuickDraw GX Bitmaps by David Surovell 
A primer on handling bitmapped graphics in QuickDraw GX: tips, tricks, and whizzy effects. 


Pick Your Picker With Color Picker 2.0 by Shannon Holland 
‘The new Color Picker Manager is flexible and customizable, allowing much tighter integration of color 
pickers with your application. 


Implementing Inheritance in Scripts by Paul G. Smith 
Supporting inheritance in your application’s scripts so that they can share handlers and global variables isn’t 
as difficult as you might think. 


COLUMNS 
BALANCE OF POWER 100 MacintoshQ &A 
Tuning PowerPC Memory Usage Apple’s Developer Support Center answers 
by Dave Evans questions about Macintosh product 
Avoiding inadvertent cache thrashing is development. 
important for maximum performance. 
110 THE VETERAN NEOPHYTE 
Designing Applications for the Power Rubber Meets Road 
Macintosh by Dave Johnson 
by Greg Robbins and Ron Avitzur Edges make the world go ’round. 
The power of the Power Macintosh means 
more than just faster spreadsheets. 112 Newton Q & A: Ask the Llama 
Answers to Newton-related development 
GRAPHICAL TRUFFLES questions; you can send in your own. 
A Cool QuickDraw GX Clipping Effect 
by Pete (“Luke”) Alexander 117 KON AND BAL’S PUZZLE PAGE 
A stroll through a snippet of code that Heaps of Fun 
demonstrates some fancy clipping. by Konstantin Othmer, Bruce Leak, and Steve 
Newman 
SOMEWHERE IN QUICKTIME Our heroes take on a guest puzzler. 
Media Capture Using the Sequence 
Grabber - 
by John Wang and Fernando Urbina 2  EDITOR’S NOTE 
‘The sequence grabber component supports 3 LETTERS 
capture of any media type. Here’s how to use it. 124 INDEX 


CONTENTS 


EDITOR’S NOTE 


Around the time I was faced with writing this editorial, I had just attended the 
celebration of my friend Mrs. Robertson’s 100th birthday, and my 85-year-old father 
had flown over from Florida to celebrate it with us. With the subject of longevity on 
my mind, I got to thinking about how it relates to develop. 


develop’s goal is to provide you with articles and code that will have a long life — that 
can live in your applications happily and compatibly even as new Macintosh systems 
are introduced. We do all we can to ensure this (at the risk of incurring the wrath of 
our authors, who may wonder why it takes so long to see something in print after 
it’s submitted to develop). We'd rather an article “have legs” than be published 
prematurely and get you into trouble further down the line. We do our best to test 
the code and get our technical reviewers’ opinions on whether a particular method is 
safe. This should be a primary concern of all developers, especially now in light of 
the whole new world of Power Macintosh systems. 


CAROLINE ROSE 


Evidence that we’re succeeding is that we still get requests to reprint articles as far 
back as Issue 2, and we often hear from readers who save every issue because they 
retain their usefulness. (Remember that, the next time you’re thinning out your 
bookshelves!) 


Our being an Apple publication gives us the distinct advantage of being able to have 
a thorough code review with future systems in mind, but at the same time it puts us 
in a unique position to have early articles on new Apple technology. So we also try to 
give you articles as soon as possible after the API for a new technology has frozen. 
And if we can, we give you a prerelease version of the new software along with the 
code on our CD. These articles may have somewhat shorter legs, but the bulk of the 
information should remain accurate for a very long time. 


In the past we’ve given you early QuickDraw GX versions and articles; now that 
QuickDraw GX has shipped, the two articles on that subject in this issue are only the 
most recent in a long line. Also in this issue we’re pleased to bring you our first 
article on OpenDoc, Apple’s new cross-platform compound-document architecture, 
even though the final version of OpenDoc will not have shipped by the time you 
read this. 


Further evidence that we’re succeeding is that we’ve again won in the International 
‘Technical Publications Competition of the Society for Technical Communication, 
this time the highest award in our category. But nothing would please us more than 
to hear from you, the most important judges of all, on what we can do to make 
develop an even better publication; please let us know at AppleLink DEVELOP. 


Cfper— 


Caroline Rose 
Editor 


2 develop Issue 19 September 1994 


CAROLINE ROSE (AppleLink CROSE) As a 
child, Caroline wrote a one-page newsletter 
about the goings-on in her neighborhood; it 
included news items, a gossip column, and a 
comic strip. Her readership was small, and the 
operation folded after one issue. She’s happy that 
develop has lasted longer than that, because 


after various jobs at Tymshare, NeXT, and Apple 
as a programmer, writer, editor, and manager, 
she feels she’s found her niche here. These days 
when Caroline's not at work she’s likely to be 
sailing, swimming, jogging, dancing, gardening, 
or otherwise not being sedentary. She hopes to 
live 100 very active years. ° 


LETTERS 


MISSING GAME FOLDER 


‘The column by Brigham Stevens on 
game development in develop Issue 17 
refers to a Game Development folder 
on the Bookmark 17 CD. I was unable 
to locate the folder. Is it me, or was 
there a production glitch? 


Also, is the folder the same one that was 
on the February Developer CD? 


— Bob Boonstra 


There was a production glitch. The folder 
was on Issue 16’s Bookmark CD but was 
inadvertently removed from Issue 17’s CD. 
We have since restored it (it’s in the Tools & 
Applications folder). We also got the name 
wrong: it’s Games, not Game Development. 


And yes, it’s the same folder as the one on 
the Developer CD. The Bookmark CD 
always contains a subset of the Developer 
CD Series. 


Happy gaming! 
— Dave Johnson 


DEVELOP ON THE SMALL SCREEN 


In Issue 17 you ask why so many 
applications lack common sense, and 
then you go on to list a number of 
annoyances caused by bad designs. I 
agree that the items mentioned could 
be better handled, but I have a similar 
complaint with develop! 


develop, shipped on CD-ROM with 
Apple DocViewer, is a software product 
that’s produced like a printed document. 
Things like double-columned pages are 
extremely difficult to read on a standard 
Macintosh 13-inch monitor — you have 
to scroll down the entire page as you 
read a single column and then scroll 


WE’RE DYING TO HEAR FROM YOU 

We welcome timely letters to the editors, 
especially from readers reacting to articles that 
we publish in develop. Letters should be 
addressed to Caroline Rose (or, if technical 
develop-elated questions, to Dave Johnson) at 
Apple Computer, Inc., One Infinite Loop, M/S 


back up and repeat the process. While 
this orientation makes sense on a 
printed page, it really bites on my 13- 
inch monitor. Also, develop is nearly 
unreadable with its serif typefaces, 
italics, etc. 


Right now you're probably shaking your 
head and saying that you don’t have the 
money to produce two versions of 
develop — one for print and one for CD. 
If so, then you also know why so many 
applications lack common sense! 


— Brooks Bell 


Certainly time and money do enter into 
design decisions, even at Apple. But there 
are still a lot of cases where common sense 
could be followed without a big hit to the 
schedule or pocketbook. 


Regarding your problem with viewing the 
CD version of develop on a 13-inch screen, 
you might try DocViewer’s “text” view (the 
icon in the tool bar that looks like a sheet of 
paper with writing, just to the left of the 
scaling pop-up menu). This view gets rid 
of all special formatting such as double 
columns. You can still look at illustrations, 
by clicking the Open button next to the 
figure caption. 


In text view you can change the font size 
and type of any structural part of the 
document, using the Format command in 
the Edit menu. For example, you can choose 
Body in the Format dialog and change the 
font of all the body text. (In the normal 
view, the scaling pop-up menu can be used 
to magnify everything.) 


Thanks for writing and giving us the 
opportunity to provide these tips. We 
welcome all gripes! 


— Caroline Rose 


303-4DP, Cupertino, CA 95014 (AppleLink 
CROSE or JOHNSON.DK). All letters should 
include your name and company name as well as 
your address and phone number. Letters may be 
excerpted or edited for clarity (or to make them 
say what we wish they did).° 


LETTERS 


4 develop Issue 19 September 1994 


QUICKDRAW GX MORPH TABLES 


The Macintosh Q&A section of develop 
Issue 16 stated that there’s no way to 

do an automatic checksum digit 
insertion using QuickDraw GX’s glyph 
metamorphosis tables. I haven’t read 
much about QuickDraw GX’s 'mort' 
tables, but I do know finite state tables. 
It’s quite feasible to build a 12-state 
table that will generate a checksum digit 
for a digit-string of any length. The key 
is to make the checksum be calculated 
dynamically instead of at the end. This 
reduces the required number of states to 
ten, plus beginning and end, which is 
well within the limits of QuickDraw GX. 


— Mark Cogan 
Developer Wannabe 


You’re quite right; you can indeed use 
morph tables to generate checksums. Other 
cyclic kinds of calculations are also possible; 
for example, a morph table could be set up 
to do pseudo-random selection of letterforms 
from a font that was designed with five 
variant forms of A-Z and a-z. When a 
letter is encountered, it would be replaced 
with its version from one of the sets. But 
regardless of the specific glyph, each time a 
glyph is processed the state advances and 
eventually loops back to the starting state. 
This is in general the template for how 
cyclic effects can be implemented with the 
QuickDraw GX morph tables. 


— Dave Opstad 
GX Line Layout Weenie 


WATCH-CURSOR PUSH-UPS 
AND BEYOND 


Regarding Dave Johnson’s bio in Issue 
16: He’s not the only one who plays with 
the cursor while waiting for the Mac. 
Here are some other silly things to do: 


° Try to fit the watch inside an empty 
horizontal scroll bar. It always overlaps 
one of the lines, either the top or 
bottom one, so if you move that single 
pixel that alternates between them, the 
watch seems to be slipping on its wrist 
belt. 


¢ While installing the system, position 
the counting hand so that it’s just 


touching a horizontal line. Don’t 
overlap the line, and the hand will seem 
to be cut from its owner, instead of 
being a ghost unterminated hand. 


— Javier Guerra G. 


Dave is not alone in doing watch-cursor 
push-ups and pull-ups. My personal 
favorite activity here is trying to find a 
place where I can do both at once. I 
remember that the old Font/DA Mover 
has a spot between two buttons where 
you can do that nicely. 


— Maarten Hazewinkel 


Hot Dawg! I knew there were others out 
there doing the watch cursor thing. P've 
heard from a half dozen or so; it’s a lot more 
common than I thought. 


I remember doing that with the old 
Font/DA Mover, too. The progress bar 
during long Finder operations also worked 
well. (Nowadays we have movable modal 
progress windows, so the watch cursor is 
gone — the price of progress.) 

Thanks for letting me know I’m not alone! 


— Dave Johnson 


UNABASHED PRAISE 


develop is an inspiring magazine. The 
layout is clean yet warm and inviting. 
The articles are relevant and the authors 
are knowlegeable. 


The on-line issues of develop are 
invaluable. This is the best on-line 
documentation I have seen, period. The 
articles look great — just like the 
magazine. And searching and setting 
filters is fast. Microsoft’s CD comes 
with a lot of files, but most of it is old, 
irrelevant, and ugly to read. 


I sure appreciate your effort. 


— Brent Foust 


We can’t thank you enough for taking the 
time to write. Letters like this keep us 
going, in more ways than one. We hope 
you're as happy with the recent changes to 
our layout; please let us know if not. 


— Caroline Rose 


Building an OpenDoc Part Handler 


KURT PIERSOL 


6 develop Issue 19 September 1994 


OpenDoc, Apple’s compound-document architecture, brings users a new, 
more powerful metaphor for working with documents. Writing code to 
support OpenDoc is a lot like writing a normal application. This article 
gives an overview of what’s involved in writing OpenDoc code and 
presents a simple working example. 


OpenDoc provides a new way to write application code for the Macintosh and a 
number of other desktop platforms. By following the OpenDoc guidelines, you can 
produce applications that share files, windows, and interface elements seamlessly. The 
process of writing an OpenDoc application, which we call apart handler, is much like 
writing any Macintosh application. There are differences as well, of course, and this 
article will help you understand them. 


OpenDoc applications are designed to allow code from several sources to cooperate 
in producing compound documents, documents that can embed almost any kind of 
content inside them. Each piece of content in the document (each part) includes its 
own part handler, the code that’s used to edit and view it. To achieve this, OpenDoc 
part handlers must cooperate in a number of ways. They must sort out how events 
are passed, where data is stored on the disk, and where drawing is allowed to occur on 
windows or printed pages. 


This article starts with a brief overview of OpenDoc and then talks about 
implementing a simple part handler. It will show you the absolute basics, much as 
‘TESample does for ‘TextEdit in the Macintosh Toolbox. You'll learn about a simple 
example of building a part handler, included on this issue’s CD: a clock that can 
handle two different display modes, digital and analog. The clock updates itself every 
second and allows the user to select the display mode from a menu. 


A quick caveat: The sample code provided on the CD is from the alpha version of 
OpenDoc, but by the time you read this, a beta version should be available. When 
you begin implementing your own part handler, you may find that some details of the 
API have changed; however, the overall structure will be the same. The sample clock, 
for instance, is specific to C++ and the alpha version of OpenDoc. The final version 
of OpenDoc will be based on IBM’s System Object Model (SOM), which will allow 
part handlers to be written in a variety of languages, both object-oriented and 
procedural. Similarly, the XMP prefix on OpenDoc class names (you'll see a lot of 
them in this article) will be changed to OD beginning with the beta release. 


KURT PIERSOL, the chief architect of OpenDoc, Apple. Kurt also likes to wear suspenders, though 
previously led the Apple Event project and was that has very little to do with his software 

an early technical lead for AppleScript. He’s architectural responsibilities. ® 

responsible for making technologies fit together at 


‘This is perhaps a good time to mention a bit more about SOM. This technology is 
the basic mechanism that OpenDoc part handlers use to communicate with one 
another. SOM solves many hard problems associated with using object-oriented 
languages, including those of subclassing across language boundaries, altering base 
classes under dynamic linking, and long-term maintenance of object-oriented APIs. 


OVERVIEW OF OPENDOC 


Before getting into the specifics of OpenDoc and how to write part handlers, we’ll 
talk about some of the basic services you'll see in OpenDoc and where your own code 
fits into the OpenDoc architecture. 


PART HANDLERS 

Part handlers are what provide OpenDoc with its ability to handle different kinds of 
content in a single document. You, the developer community, will write the various 
part handlers that plug into OpenDoc. 


Part handlers are a lot like existing applications. They handle events, draw and print, 
and read and store data onto disk. Every part handler provides a series of entry points 
that allow OpenDoc to request any of these actions from the part handler. In 
addition, the API has a number of “bookkeeping calls,” which allow OpenDoc to 
provide undo services and notify part handlers when their environment has changed. 


Overall, there are about 50 calls in the OpenDoc part API that a part needs to 
implement. This is a lot, but it actually maps fairly closely with the number of things 
you'd have to do to write any Macintosh application. In addition, you can ignore 
many of these calls in many cases. For instance, if you don’t allow embedding of other 
parts within your part, there are about ten calls that you can safely ignore. If you 
don’t update your display asynchronously, but simply wait for update calls, there are 
additional calls that you can ignore. In many typical cases, this means that you can 
build a part handler very quickly from existing code. 


Part handlers are packaged as shared libraries in the Macintosh version of OpenDoc. 
This won't always be the case on other OpenDoc platforms, but you can count on the 
API being the same on all platforms. The alpha version of OpenDoc uses the Apple 
Shared Library Manager to dynamically link your part handler into OpenDoc, while 
the beta version will use SOM. These versions will have different linker behavior but 
will essentially require the same basic packaging of your code: a shared library. 


In either case, you'll find that OpenDoc is an object-oriented API. That means 
yow ll be talking to OpenDoc objects, and your part handler will itself be an 
OpenDoc object (or set of objects). This doesn’t mean that your code has to be built 
from the ground up in C++, though. SOM will provide interfaces to many languages, 
including C. 


Because part handlers are themselves objects, we often refer to them as “part objects” 
or “parts” in conversation. In fact, what the user would call a “part” in a document is 
really the combination of some persistent data stored in the document file and a set of 
objects that OpenDoc uses to display and manipulate the stored data. OpenDoc 
chooses appropriate part handlers based on the type of data stored in the document. 


RUNTIME OBJECTS 

As we describe how to write a part handler, we’ll mention some runtime objects that 
interact with your code. In OpenDoc, these objects can be located at run time using 
the session object (XMPSession), to which your part object will be given a pointer 


BUILDING AN OPENDOC PART HANDLER 


8 develop (Issue 19 September 1994 


when it’s initialized. The session object is very important because it’s your link to the 
rest of the OpenDoc objects that are running in the document. 


There’s a whole list of objects that the session object makes available. Of these, only 
three will be important for the purposes of this article: the arbitrator, the dispatcher, 
and the undo stack. 


¢ The arbitrator is an object of class XMPArbitrator. The arbitrator 
for a session is the place where part handlers register their 
ownership of certain resources. ‘The menu bar, the keystroke 
stream, and the current selection are all examples of resources that 
the arbitrator tracks. 


¢ The dispatcher is the object that dispatches events to the various 
part handlers. It’s an object of class XMPDispatcher. It’s used in 
our example as a way to register for background time. 


¢ The undo stack is an object of class XMPUndo that allows 
OpenDoc to support multilevel undo across part handler 
boundaries. 


Each of these objects will be discussed as it’s encountered. 


A RUNNING START 


‘To give you a running start, we’ve built a small object-oriented framework for parts 
that implements the direct interface to OpenDoc. This framework is a precursor to 
the new part handler framework that Apple is building, and is included here simply as 
sample code. Our sample clock uses this framework. The good thing about the 
framework is that it clearly separates the work that any part handler must do to be 
OpenDoc compliant from the specific work performed in putting up a clock. 


The framework divides the work of a part into three objects: a frame object, a facet 
object, and a part object. OpenDoc itself doesn’t require that you create anything but 
a part object, but for the sake of clarity the framework divides the labor among 
several smaller objects. For easy reference, here’s a list of the classes we’ll be 
discussing throughout the article and their corresponding source files, included on 
the CD: 


CPart FWPart.h, FWPart.cpp 
CFrame FWFrame.h, FWFrame.cpp 
CFacet FWFacet.h, FWFacet.cpp 
CClockPart ClockPar.h, ClockPar.cpp 


CClockFrame ClockFra.h, ClockFra.cpp 
CClockFacet ClockFac.h, ClockFac.cpp 


The classes defined by the framework generally start with the letter C, hence the 
classes CPart, CFrame, and CFacet. These three parts are helper objects for three 
OpenDoc classes, XMPPart, XMPFrame, and XMPFacet. XMPPart objects are 
OpenDoc part handlers: you’ll subclass XMPPart when writing your own. OpenDoc 
uses the frame and facet objects to help part handlers lay themselves out in a window. 
How these classes work together is probably the single most complex thing to 
understand in OpenDoc. 


XMPPart, the class from which part handlers are derived, is simply the base class of 
every OpenDoc part handler. It’s the class that actually handles the drawing, editing, 
and storage. Every part handler is an implementation of some subclass of XMPPart. 


CPart, in the framework, is a class derived from XMPPart. It’s just a default 
implementation of the basic XMPPart behavior. As such, CPart is a treasure trove of 
information about the correct way to “ignore” calls that aren’t interesting because 
your part handler doesn’t support embedding, update asynchronously, or use 
offscreen bitmaps. 


Every part is embedded in another part, with the exception of the cot part, the top- 
level part in each compound document. When a part is embedded in another part, 
there’s an object that’s used to store information about the shape of the embedded 
part. This boundary between a container and an embedded part is a frame — an 
instance of the class XMPFrame. Every frame has a single part displayed inside it. 
‘The container actually embeds the frame; it knows nothing about the part inside. 


Any part can be displayed in several frames at the same time. This makes it easy for a 
part to be visible in several windows or to have several different presentations. For 
example, a charting part might want to have one frame displaying the chart and 
another allowing the data to be edited in a table. 


A facet (an instance of an XMPFacet object) is a visible part of a frame. There can be 
many facets displaying within any given frame. This is a useful property, for instance, 
when a container wants to “split” windows. 


Both XMPFrames and XMPFacets have a field, partInfo, for storing information 
specific to the part being displayed. This is rather like a window refCon, a handy 
place to store information independent of the object itself. The CFrame and CFacet 
objects are designed to be plugged into the partInfo fields of their XMPFrame and 
XMPFacet counterparts. The containing part creates the XMPFrame and XMPFacet 
objects and then allows your part handler to initialize their partInfo fields. In the 
framework, the actual work of drawing the part on the screen is done in the CFacet 
object. The work of deciding what shape the embedded part will take is done in the 
CFrame object. As we describe the specific operations, we’ll point out the class in 
which the code resides. 


INITIALIZATION CODE 


The first bit of code we’ll consider is the initialization code for each part object. Each 
distinct part in a document gets an instance of the part object, so if there are seven 
little clocks running in different windows (or the same window, for that matter) there 
are seven instances of the clock part object. This means that you probably want to 
come up with a scheme to share any global data so that you aren’t wasting space with 
many copies of it. Both the Apple Shared Library Manager and SOM support 
systemwide global storage, so this should be straightforward. 


Resources are a special case. You’ll want to be very polite about not permanently 
fiddling with the resource chain or making assumptions about where your resource 
file is in the chain. We suggest saving the previous head of the resource chain, setting 
your file to be the end of the chain, and using the single-level resource calls (such as 
GetlIndResource) to find the resources you're after. Since you'll probably want to 
share the resources among separate instances of your part object, it may be better to 
detach the resources you get and manage them yourself instead of counting on any 
particular application heap to have the correct resource map. 


THE CONSTRUCTOR 
The first step in initialization is the constructor. You should never do anything that 
could possibly fail in a constructor. This pretty much limits you to operations like 


BUILDING AN OPENDOC PART HANDLER 9 


10 develop (Issue 19 September 1994 


setting pointer variables to NULL, setting numeric variables to appropriate values, 
and making similar assignments from constants. 


You can see a good example in ClockPar.cpp. The clock part simply sets up its fields 
with appropriate constant values. 


XMPPART::INITPART 

The next phase of initialization takes place in the InitPart method that every part 
object implements. The InitPart method is called by OpenDoc after the part object 
has been created, and here you can attempt things that can fail. This is where you 
should attempt to allocate any extra memory you need for your part instance, get 
resources if you need them, and set up your persistent storage. 


Let’s examine how OpenDoc’s storage system looks to a part handler. When your 
part object is created, the InitPart method is passed a storage unit object in which you 
can persistently store information. A storage unit is really just a list of named 
properties, each of which has one or more values. Each value is an entire stream, like an 
existing Macintosh file. You can do read, write, seek, insert, and delete operations on 
individual values. 


Each value has a type, much like the type code associated with a Macintosh file. Every 
property in a storage unit can have one or more values, each with their own type 
code. Thus, you can store multiple representations of any property. You can make up 
any property names you like. One special property name, kXMPPropContents, is 
used by OpenDoc to determine which handler goes with which part at run time. 
Every part object should have a property named kXMPPropContents so that OpenDoc 
can determine what part handler to run. 


In our sample, CClockPart has an Initialize method, which is called by CPart::InitPart. 
It sets up the menu bar for the clock and sets up a focus set for obtaining system 
resources from the arbitrator (more about this later). A good example of code to set 
up persistent storage can be found in the implementation of CPart. The framework 
calls its own method, called CheckAndAddProperties, to make sure that the storage 
unit is set up correctly. 


DRAWING CODE 


Now that your initialization code is in place, you’ll want to make sure you can get 
your part to draw onscreen. OpenDoc will call your part with the Draw method and 
tell you which facet should be drawn. 


Our sample, CClockPart, inherits some code from CPart that asks the CFacet object 
to do the drawing. Notice, though, that before it does this, CPart::Draw sets up the 
graphics port for drawing using the clipping information from the facet. This is very 
similar to the basic drawing model for the Macintosh, where you draw using the 
appropriate graphics port and clipping region. You can find the rest of the drawing 
code in CClockFacet::Draw. This code consists of just the straightforward QuickDraw 
calls and attendant calculations needed to display either the digital or the analog clock 
face. 


We use a utility class called CDrawlInitiator to set up the drawing environment 
reliably. The constructor of this class does all the work of setting the graphics port’s 
clipping region and origin. Later, the destructor restores the port to its previous state. 
This is a tricky bit of C++ coding that takes advantage of the object allocation 
behavior of stack-based objects in C++. 


HANDLING LAYOUT 

One of the features of CClockPart is that it presents a round shape when it’s 
embedded. To do this, it uses the XMPFrame object’s layout negotiation features. 
‘To understand this, you need to understand the notions of canvas, shape, and 
transform in OpenDoc. 


¢ A canvas is simply a drawing context. On the Macintosh it can be 
either a QuickDraw graphics port or a QuickDraw GX view port. 


¢ A shape is a way of describing an area of a canvas. OpenDoc 
supports describing shapes in terms of polygons, regions, or 
rectangles on the Macintosh. 


¢ A transform is a geometric transformation appropriate to the type 
of canvas in use. In QuickDraw, the only transformation available 
is an offset. In QuickDraw GX, the transformations are much 
more powerful, capable of scaling, rotating, and offsets, as well as 
some more interesting transformations such as skewing. 


An OpenDoc frame has a set of shapes associated with it. One, called the frame shape, 
is how the container tells an embedded object how to lay itself out. Your part handler 
should use the frame shape to decide what to display and how to lay it out. When 
your part is finished laying itself out, it can optionally specify to the container exactly 
what part of the frame shape it plans to use. This shape is called the wsed shape of the 
frame. Finally, the embedded part can specify an active shape, which is the subset of 
the frame shape that you want to use for determining whether you receive a mouse 
event. Often the used shape and the active shape are set to be the same shape. The 
container can take care of filling in any areas left untouched by the part handler. 


For example, assume we have a clock part embedded in a word processor that does 
text wrapping. The word processor allows its embedded frames to be laid out as 
rectangles. When the clock is embedded, it uses the frame shape (the rectangle given 
by the word processor) to determine the size of the clock face. After determining the 
size, it sets its used and active shapes to match the shape of the round clock face. The 
word processor is now free to wrap text around the round clock face, and any clicks in 
the rectangular frame shape that aren’t actually on the clock face are passed through 
to the word processor. Those clicks can be used to manipulate the text that’s wrapped 
close to the clock face. 


CClockPart negotiates to get the frame shape to match the clock’s round face. This 
works but is not strictly necessary. Instead, it could simply set its used shape to match 
the round area of the clock. Either method will ensure that the container knows how 
to clip any underlying parts so that they don’t draw in the clock’s area. 


A quick aside about shape negotiation: Negotiation is rather straightforward in 
OpenDoc, but knowledgeable programmers will notice that there is little support for 
constrained negotiation. This is not an oversight, but instead a fundamental design 
choice. It’s up to the embedding part to constrain layout according to its model of 
content. This means that constraint strategies like “Boxes & Glue” or “Springs & 
Wires” are the province of your part handler, not OpenDoc. You can implement any 
of a number of layout constraint schemes on top of OpenDoc, but every part handler 
may constrain layout within its own frame. 


You can see what happens when the container reshapes the clock in the method 
CClockFrame::FrameShapeChanged. CClockFrame requests a round shape from the 
container and then invalidates the correct areas so that redrawing occurs. For most 
parts, the standard behavior is to redo the layout based on the new shape, update the 
active and used shapes of the frame, and then invalidate the proper areas. 


BUILDING AN OPENDOC PART HANDLER 


1 2 develop (Issue 19 September 1994 


EVENT HANDLING 


Our next area of implementation is the event handling for the clock part. This is 
much like writing the event handling for any Macintosh application, with one 
difference: you don’t poll the system for events by calling WaitNextEvent. Instead, 
when there’s an event for your part handler OpenDoc calls your part’s HandleEvent 
method. 


XMPPART::HANDLEEVENT 

The code inside HandleEvent is usually a switch statement, just as in applications 
today. There are some minor differences, which are nicely illustrated by the code that 
CClockPart inherits from CPart. This code effectively delegates the various events to 
code that can handle each event, using a switch statement. Notice the behavior for 
mouse-down events, which calls CFacet::HandleMouseDown, which in turn causes 
the frame to become active if it isn’t already. 


The notion of activation in OpenDoc is closely tied to the object discussed briefly 
earlier, the arbitrator. An object is “active” if it owns some of the foci in the arbitrator. 
A focus is just a shared data structure or system service of some kind, such as the menu 
bar or keystroke stream. 


When CClockFrame is told to activate, it requests a set of foci from the arbitrator. 
In this case, it wants the menus and selection focus. These two, with the addition of 
the keystroke stream, constitute the basic focus set that almost every part asks for 
when it wants to allow editing. You can find the code that sets up the focus set in 
CClockFrame::InitClockFrame. In CFrame::ActivateFrame, the focus set is 
requested. 


Notice that the part is requesting the menu focus before it attempts to put up its 
menu bar. This is the basic rule to follow in all cases. If there’s an arbitrator focus for 
the resource, you must request it and succeed in getting it before it’s OK to use the 
resource. OpenDoc uses the arbitrator to carefully manage the sharing of data and 
system services, so it’s very important to do the right thing and ask for foci whenever 
you need shared resources. 


PUTTING UP A MENU BAR 

One of the things that CClockPart does is to set up a menu bar. You can see the code 
for this in CClockPart::Initialize. The initialization code gets a reference to its menu 
bar object and then calls the AddMenu method of the menu bar object to add its 
menus. Finally, it registers command IDs to pass back when menu items are selected. 


OpenDoc provides a menu bar object to help you set up menus and display them 
when your part has obtained the menu focus. The major reason for this object to exist 
at all is to support compatibility with Microsoft’s proprietary OLE 2.0 document 
architecture. This object hides the complex menu-mixing behavior of OLE 2.0 
behind a simple interface that works correctly in either an OpenDoc or an OLE 2.0 
container. 


Later, during execution of CFrame::FocusStateChanged, the menu bar object is asked 
to display itself. The actual code invoked is in CPart::InstallMenus, and basically just 
calls the Display method of the menu bar object. 


GETTING IDLE TIME 

You can get background time, delivered as idle events, on any of your frames. This is 
done by getting the dispatcher from the session object and registering particular 
frames for idle time. 


You can see an example of this sort of registration in CClockPart::Initialize. In this 
case, the part itself is registering to receive idle events, but individual frames can also 
be registered. Once a frame or part has been registered for idle time, it will receive 
idle events in its HandleEvent method. 


UNDO AND REDO 

Although CClockPart is too simple to support undo, it’s worthwhile to look at how 
you would go about adding undo support to your OpenDoc part handler. We’ve 
tried to make it as simple and unobtrusive as possible to do multilevel undo in 
OpenDoc. 


The first step is to create code that tells OpenDoc you’ve done something that can 
be undone. You do this by getting the undo object, an instance of XMPUndo, from 
the session object. You then call the XMPUndo::AddAction ToHistory method, 
which takes a hunk of data that you create to hold instructions about how to undo 
the latest action. OpenDoc never looks inside this hunk of bits; it merely stores it for 
later. 


The code might look like this: 


fSession->GetUndo()->AddActionToHistory(thisPart, myUndoData, 
kXMPSingleAction, myUndoString, myRedoString) 


myUndoData is a pointer to the undo data, and myUndoString and myRedoString 
are strings to show in the Edit menu, to tell the user what action will be undone or 
redone. 


Once the information is on the undo stack, simply calling the XMPUndo::Undo and 
XMPUndo::Redo methods will cause the system to send the correct messages to the 
parts to get the last action undone. This allows the user to undo actions that were 
made in other parts, without your part knowing precisely what needs to be done. 


When the undo object is told to undo or redo, it calls your part handler back using 
the Undo or Redo method. If you never post undo actions, you never need worry 
about having these methods called, and you can ignore them. The XMPUndo object 
will always return exactly what you store in it, and it makes sure that undo and redo 
operations are invoked in the correct order. When the Undo object is finished with 
the undo data, it asks your part to dispose of it by calling your part’s DisposeActionState 
method. This means that you can safely put pointers to other data into the undo data, 
since you'll get a chance to dispose of the data, and anything it points to, at a later 
time. 


On some systems, such as one that supports persistent undo stacks, you may be asked 
to read and write your undo data against a persistent storage medium. This is not the 
case on the Macintosh, but OpenDoc does allow for it. You can safely ignore this 
until it becomes an issue on some platform you choose to support. 


STORAGE 


Eventually it becomes time to save a document. We’ve already discussed the OpenDoc 
storage environment to some degree. The storage unit object in OpenDoc is set up 
for the part by the OpenDoc libraries themselves, so generally a part never needs to 
talk directly to the file system just to read and write its own data. This system 
supports not only compound document storage, but also a versioning system that 
allows for multiple drafts. 


BUILDING AN OPENDOC PART HANDLER 


13 


14 develop (Issue 19 September 1994 


DEALING WITH STORAGE UNITS 

Once you’ve been given a storage unit, you typically get it ready by using the Focus 
call. To minimize the API, a set of common functions that can apply to the entire 
storage unit, a particular property, or a particular value has been abstracted out. 
Properties and values within a storage unit are not represented by distinct objects, but 
are instead captured in the focus state of the storage unit: the Focus method sets up 
the context for later calls. For example, the Remove method can apply to an entire 
property or to a single value of it, depending on whether the storage unit was focused 
on the property or on a value. Focusing can be absolute (when you pass a particular 
property ID or value index) or relative (when you pass a position code). 


The read operation is performed with the XMPStorageUnit::GetValue method, and 
the write operation with XMPStorage Unit::SetValue. The position can be set or 

read with XMPStorage Unit::SetOffset and XMPStorage Unit::GetOffset. Efficient 
inserts and deletes can be performed with XMPStorageUnit::InsertValue and 
XMPStorageUnit::Delete Value. You can also use the latter call to truncate a given value. 


Typically, your part will focus on the kXMPPropContents property and do various 
reads or writes, depending on whether your part is being internalized (read in from 
storage) or externalized (written out). If your part is sufficiently large and complex, 
you'll probably want to use inserts and deletes to store changes to your persistent 
data. This has two useful effects: it makes your data more randomly accessible, and it 
makes the OpenDoc draft system store changes more efficiently. 


This draft system allows a user to save a draft of a document and return to view the 
draft at any future time. Where possible, it stores only the changes between 
succeeding drafts, instead of storing entire copies of the document for each draft. By 
using OpenDoc’s storage APIs, you automatically get this efficient storage of separate 
versions with no additional work on your part. OpenDoc only watches the storage 
operations, though; it doesn’t attempt to detect differences on its own. If you use 
insert and delete operations, OpenDoc’s storage system can efficiently store the 
changes between drafts. 


BASIC I/O FOR YOUR PART 

When your part is brought into memory, your InitPartFromStorage method is called, 
and it’s passed a storage unit. You are then responsible for reading the storage unit 
and getting ready to receive other messages. This will happen once, and never again 
until the object is deleted from memory. Later, when the document is being saved, 
your part’s Externalize method is called. You must immediately write anything you 
need to store persistently out into your storage unit, before returning from this 
method. 


Your part is also free to write to its storage unit, as well as read from it, whenever it 
wants to. For part handlers that “virtualize” themselves from disk, this means that 
OpenDoc won’t get in your way. 


The CClockPart::InternalizeContent and CClockPart::ExternalizeContent methods 
are called by the framework in response to the standard methods InitPartFromStorage 
and Externalize. They demonstrate focusing a storage unit and doing read and write 
operations. CClockPart’s storage needs are very simple; it just reads and writes a few 
flags into its storage unit. 


PART INFORMATION ATTACHED TO FRAMES 
As mentioned earlier, the XMPFrame objects associated with embedding have a 
partInfo field, which is used like a window refCon by your code. When the document 


is saved, you may be asked to save the contents of this partInfo field to a particular 
storage unit. Your part will be called using the XMPPart::WritePartInfo method. 
Your responsibility is to write enough information to be able to reconstruct the 
partInfo field. Later, when the document is reopened, your part object will be called 
with the XMPPart::ReadPartInfo method. This is your cue to read the data back into 
memory and set up the part info for that frame object once again. 


These partInfo fields are useful when you want to write a part that can have several 
visible frames, each with a different presentation. The chart example we used earlier 
is a case in point. We would want to allow a chart to be viewed as a table of data or a 
chart, possibly one of various chart kinds. By storing information about what to 
display in the frame’s part info, you're freed from writing your own data structure to 
remember what kind of display to do in what frame. Instead, you store that 
information as a part of the frame’s part info and implement WritePartInfo and 
ReadPartInfo methods to save and restore the data. 


CClockPart doesn’t actually use the partInfo field of its frames in a persistent fashion. 
It simply inherits code from CPart, which reconstructs the appropriate CFrame and 
CFacet objects at run time. This is completely adequate for simple parts. 


BEING A GOOD OPENDOC CITIZEN 


Now that we’ve covered the basics, there are a few last details to implement before 
we’ve got a good basic part. Since a part can have multiple frames, and a frame can be 
visible in multiple facets, we need to make sure our part handler does the right thing 
and avoids stepping on the toes of other parts. 


ADDING AND REMOVING FACETS 

When a part becomes visible (that is, when a facet appears), OpenDoc notifies the 
part with a call to the FacetAdded method. This is when your part should do any 
special setup it needs to (for instance, you may want to register for idle time on the 
frame associated with that facet). Similarly, OpenDoc calls your part handler’s 
FacetRemoved method when the facet goes away; here you should clean up any 
actions you took in response to FacetAdded. 


ADJUSTING MENUS 
When your part handler acquires the menu focus, OpenDoc calls its AdjustMenus 
method. Your job is to correctly update the menus so that the right elements are 


checked, enabled, and so on. You can see an example in CClockPart::DoAdjustMenus, 
which is called by the inherited code from CPart::AdjustMenus. 


RELINQUISHING ARBITRATOR FOCI 

Once you’ve acquired any focus from the arbitrator, you'll eventually be called on to 
release it. This will happen via three methods: XMPPart::BeginRelinquishFocus, 
XMPPart::CommitRelinquishFocus, and XMPPart::AbortRelinquishFocus. The first 
method is called to ask your part if it’s willing to relinquish a focus it owns. It should, 
if at all possible, say yes. It’s possible, though, that you won’t give up a focus, because 
your part object is in a mode. For instance, you wouldn’t give up the serial port focus 
if you were in the middle of an XMODEM transfer. 


Once your part has responded to the XMPPart::BeginRelinquishFocus call, you 
can expect another call shortly after that which informs your part that the focus has 
really been given to another frame, or that it hasn’t. The first case is signaled by 
XMPPart::CommitRelinquishFocus, and the second case is signaled by 
XMPPart::AbortRelinquishFocus. 


BUILDING AN OPENDOC PART HANDLER 


15 


16 develop lssue 19 September 1994 


Occasionally, under difficult conditions, your part will simply be informed that it has 
either acquired a focus (through the XMPPart::FocusAcquired method) or lost a 
focus (through the XMPPart::FocusLost method). If your part has lost a focus, you’re 
expected to avoid inappropriate behavior, such as attempting to adjust menus or 
display a menu bar when you don’t have the menu bar focus. 


FREEING MEMORY 

Your part is expected, if possible, to free some memory on request. When it’s needed, 
you'll be called with the XMPPart::Purge method. You're given a size that’s the 
amount of memory requested. If you can manage it, you should free any unneeded 
memory from your part’s data structures. Don’t free anything you need to keep 
running, of course. You might free any resources you were holding, or free some 
cached data. CClockPart, our example, is so simple that it has almost nothing to 


purge. 


IN CLOSING 


By now you should have a good idea of what’s involved in writing an OpenDoc part 
handler. As you’ve seen, it’s much like writing an application today: you still write 
code to handle events, deal with storage issues, draw to the screen, and so on. The 
main differences are really in the “packaging” of the code and in the environment it 
runs in. (Some previously messy areas have even been cleanly abstracted for you. The 
storage system is a good example: no more ugly file handling code; you just deal with 
storage units and let OpenDoc handle the details.) 


But the differences for users are amazing. No more worrying about which application 
can open which document. Instead, when they select a particular type of content to 
work with, the tools they need to work with that content simply appear. In user tests, 
many people thought that this radically wonderful technology was just a bug fix, and 
that it was finally working the way it was always supposed to. There can be no better 
indication that OpenDoc is a step in the right direction. 


Thanks to our technical reviewers David Austin, 
Ray Chiang, Mark Minshull, Alan Spragens, and 
Borek Vokach-Brodsky. °® 


BALANCE OF 
POWER 


Tuning PowerPC 
Memory Usage 


DAVE EVANS 


If you care about the performance of code you write for 
the Power Macintosh, memory usage should be your 
foremost concern. With the PowerPC™ 601 processor 
today, and even more important with future processors, 
memory usage of your code will have the greatest effect 
on its performance. Poorly written code will execute at 
a fraction of its potential, and often very simple 
changes will greatly improve the execution speed of 
your critical code. 


Processors are improving much faster than the memory 
subsystems that support them. As the PowerPC chips 
move from 80 MHz to 100 MHz and beyond, their 
thirst for data to process and instructions to execute 
will increasingly tax memory. Memory caches attempt 
to mitigate that thirst, and all PowerPC processors 
come equipped with built-in caches. But your code can 
work well with a cache or it can work very poorly with 
a cache. I'll show you why and discuss what you can do 
to optimize your memory usage. 


CACHE BASICS 

As you know, a cache is simply very fast RAM that the 
processor can access quickly and that it uses to store 
recently referenced data and code. On the PowerPC 
processors, any data stored in the cache can be accessed 
without stalling the processor’s pipeline. Accesses to 
data not in the cache will take about 20 times as long 
reading from main memory, or even | million times as 
long if the access causes a page fault with virtual 
memory. Getting and keeping your performance- 
critical code and data in the cache are therefore key to 
your execution speed. 


A cache is divided into small blocks called cache lines. 
On the PowerPC 601, for example, the cache has 1024 


cache line blocks, each holding 32 bytes. In addition, 
the 601 will fetch two blocks when it can, making the 
cache line size effectively 64 bytes. 


The first PowerPC processors have set associative 
caches of different sizes. The 601 has an eight-way set 
associative, unified cache that’s 32K in size, and the 603 
has a two-way set associative, split cache with 8K for 
data and 8K for instruction code. The term set 
associative refers to the way the cache relates to main 
memory, which is important to your performance. In 
some simple caching schemes, each cache line maps 
directly to specific areas of main memory; any access to 
one of these areas loads bytes into that cache line. But 
on the PowerPC processor, sets of cache lines are 
combined and then mapped to memory. There are 
eight cache lines in each set on the 601, and two in 
each set on the 603. An access to one of the areas 
mapped to a set will load bytes into the last-used cache 
line of the set, keeping the most frequently used cache 
lines from being purged. This more complicated 
scheme typically yields much better performance than 
the directly mapped cache. 


CACHE THRASHING 
The cache will most affect your performance when 
youre accessing large amounts of data. A typical 
example of this is walking through arrays to perform 
some operation. The best strategy is to minimize cache 
collisions during your accesses, and the best tactic for 
this is to access your data as sequentially as possible. If 
you walk through memory sequentially, you’ll load the 
cache every 64 bytes, but all 64 bytes will be available 
for fast processing. Here’s an example: 
unsigned long data[64][1024]; 
for (row = 2; row < 64; rowtt) 
for (column = 0; column < 1024; column++) 
data[row][column] = data[row-1][column] + 
data[row-2][column]; 


This example performs additions on each element of a 

large matrix and accesses that matrix sequentially in 

memory. It walks across each row, adding elements and 

storing the result. But just inverting the loops can 

significantly change the way memory is accessed: 

unsigned long data[64][1024]; 

for (column = 0; column < 1024; columnt++) 

for (row = 2; row < 64; rowtt) 
data[row][column] = data[row-1][column] + 
data[row-2][column]; 


DAVE EVANS occasionally uses the combinatorics skills he 
learned at the Massachusetts Institute of Technology, but more often 
he’s been practicing his combination punches at a Thai kickboxing 


gym. Designing fast algorithms for Apple’s OS Platforms Group is 
definitely rewarding, but developing a fast left hook really gets him 
pumped up. ° 


BALANCE OF POWER: TUNING POWERPC MEMORY USAGE 


17 


Reversing the loops leads to less than optimal 
performance since we perform each addition for all the 
columns before moving to the next element of a row. 
Instead of sequential access, this access pattern jumps 
across memory in even steps of 4K. Unfortunately, on 
the PowerPC processor these accesses map to the same 
set of cache lines, and every operation causes the cache 
to reload from main memory. This second example 
takes twice as long to execute the same calculation on a 
Power Macintosh 6100/60. 


By paying attention to how your code accesses memory, 
you can avoid serious cache thrashing like that done by 
the second example. Things to look out for are loops 
that iterate for a power of 2 steps (128, 256, and so on) 
and code whose memory accesses are not close together. 


An approach called blocking may help your loops. Often 
your code isn’t as simple as above, and your memory 
accesses aren’t regular during the loop. If you’re 
walking two different arrays with different increments 
through memory, it may be impossible to serialize your 
accesses. Blocking performs the calculations in blocks 
of rows and columns. Instead of iterating across all the 
columns and then proceeding to the next row, you 
divide the dimensional space into blocks and calculate 
one whole block at a time. In this next example, we 
calculate the multiplication of two matrices. 


long result[64][64], foo[64][128], bar[128][64]; 
for (row = 0; row < 64; rowtt) 
for (column = 0; column < 64; column+t+) { 
long sum = 0; 
0; i < 128; i++) 
sum += foo[row][i] * bar[i][column]; 


for (i = 


result[row][column] = sum; 


As this algorithm walks through memory, it accesses 
result and foo sequentially, but bar is accessed in 256- 
byte steps. Accessing bar by jumping through memory 
causes cache misses, and sequential elements of bar are 
flushed from the cache before they’re needed. 


By performing this operation in small blocks, we can 
better use the cache. The key is to use all the elements 
of foo and bar that are in a cache line before moving 
on. One way to do this is to expand the loop and 
perform four operations in a single iteration: 


long result[64][64], foo[64][128], bar[128][64]; 
for (row = 0; row < 64; rowtt) 

for (column = 0; column < 64; column += 4) { 
0; 
long sum3 = 0, sum4 0; 
for (i = 0; i < 128; itt) { 


long suml = 0, sum2 


1 8 develop Issue 19 September 1994 


suml += foo[row][i] * bar[i][column]; 


sum2 += foo[row][i] * bar[i][columnt+1]; 


sum3 += foo[row][i] * bar[i][column+2]; 


sum4 += foo[row][i] * bar[i][column+3]; 


} 

result[row][column] = suml; 
result[row][column+1l] = sum2; 
result[row][column+2] = sum3; 
result[row][column+3] = sum4; 


‘This expanded loop calculates a block of four cells in 
each iteration. This executes faster because elements of 
bar are read from the cache and don’t always cause 
cache misses as in the earlier example. Notice that in 
the expanded inner loop, a cache line of the bar matrix 
will be loaded the first time that it’s referenced; then 
the following three references to bar will occur without 
stalling. Using the bar elements while they’re still in 
the cache gives us a significant improvement. 


CODE STYLE 

Good compilers can pay attention to your memory 
accesses and will optimize how you access memory. For 
example, load and store operations can be reordered by 
the compiler to occur when the data is most likely to be 
available. The first time data is accessed it tends to 
cause a cache line to load, and subsequent accesses to 
nearby data must also wait for the cache load to 
complete. The compiler may be able to help by 
inserting a few instructions between the loads. This 
way the cache line will be fully loaded when the 
subsequent accesses are needed. 


For more information on loop expansion and instruction 
reordering, see the Balance of Power column in develop Issue 18.° 


You can help your compiler by using local variables 
when you can. These tell the compiler exactly how the 
data will be used, enabling it to easily reorder the loads 
and stores for this data. 


You should also carefully note memory dereferences, 
especially double dereferences. Although it may be 
obvious to you, the compiler often can’t tell whether 
two pointers address the same object in memory. The 
compiler may be prevented from reordering 
instructions because it can’t tell whether two operations 
are really dependent on each other, just because they 
contain dereferences. Here’s an example: 


paramBlock->size = myStructure->size; 
paramBlock->offset = myStructure->offset; 


Although it appears obvious, the compiler usually can’t 
tell if paramBlock references the same memory as 


myStructure. In the resulting binary, the compiler will 
be conservative and not reorder these operations for 
best execution. Replacing the dereference of 
myStructure with local variables for size and offset will 
allow the compiler to fully optimize this example. 


INSTRUCTION THRASHING 

Your code binary itself can cause the cache to thrash as 
it loads to be executed. This is very hard to detect and 
optimize. The basic problem is that your subroutines 
may map to the same areas of the cache, and frequent 
calls among them will stall to reload the cache. Some 
code profilers for RISC workstations have attempted to 
detect this problem, but for the Macintosh I can’t 
suggest much help. Just changing the link order of your 
code and then executing profiles may have an effect; 
some link orders will thrash more than others. 


DATA STRUCTURES 

The layout of your data structures can greatly affect 
your cache usage and your memory usage in general. 
For example, memory accesses that cross 64-bit 
memory boundaries take twice as long to process, as 
this forces two bus transactions. On the PowerPC 601 


processor, any misaligned data access within a memory 
boundary takes the standard amount of time, which 
(because of typical Macintosh data structures) is a 
valuable feature of the chip; future PowerPC 
processors, however, may take longer to access 
misaligned data. If you can align your data structures, 
do so now. A good tactic is to keep 64-bit data at the 
top of your structure, followed by your 32-bit data, and 
so on to prevent accidental misalignment of elements. 
Pad the end of the structure to an even 64-bit 
increment if you will have arrays of structures or will 
allocate them on the stack. And if certain parts of your 
structure are accessed much more often than other 
parts, keep these together so that they stay in the cache, 
and make sure they’re aligned. 


DON’T FORGET 

‘The memory usage of your speed-critical code will 
greatly affect its performance today, and current 
problems will just get worse when PowerPC processors 
go above 100 MHz. Profile your code to find the most 
critical bottlenecks; then pay close attention to how 
that code addresses memory. You’ll be rewarded with 
an excellent return on your investment. 


Thanks to Tom Adams, Mike Cappella, Rob Johnston, and Mike 


Neil for reviewing this column. ® 


we S$ 


g wy YY 


Power up with 
PowerPC 


Apple Developer University has just 
what you need to jump start your PowerPC 
development efforts. 


¢ Programmer's Introduction to RISC and 
PowerPC self-paced training 


¢ PowerPC BootCamp 


For more information, contact the Developer 
University Registrar at (408) 974-4897. 


WSS EY, SS 


BALANCE OF POWER: TUNING POWERPC MEMORY USAGE 


19 


Designing 
Applications for 
the Power 
Macintosh 


GREG ROBBINS AND 
RON AVITZUR 


‘The Power Macintosh offers surprises both for users 
and for developers. Users notice that it’s a fully 
compatible Macintosh and that native applications run 
blazingly fast. Developers, upon learning how the 
Power Macintosh differs from a 680x0-based 
Macintosh, discover that it’s still basically a Macintosh. 
But the Power Macintosh can offer a much richer 
experience than was possible with previous computers 
if developers break free of their old assumptions and 
harness the power of the machine to make software not 
just faster, but easier and more enjoyable to use. 


‘The Graphing Calculator desk accessory that ships 
with the Power Macintosh was designed to take 
advantage of the machine’s power to make some 
challenging mathematical tasks easily accessible — in 
particular, algebraic manipulation and 2-D and 3-D 
graphing. In this column we’ll share some lessons we 
learned when writing the Graphing Calculator. We'll 
speak about software design rather than programming 
details because we frequently discovered that our 
intuition and the standard approaches to many aspects 
of the application design were no longer appropriate. 


In general, we learned that it’s time to break free of the 
idioms developed for the machines of 1984 and begin 
designing a new generation of software. We’re not in 
Kansas anymore. The lowest common denominator of 
hardware — where developers usually aim in order to 
maintain consistency across all Macintosh models — 
has changed. The target has typically been an 8 MHz 
68000 processor or, for color applications, perhaps a 
machine twice as fast. The lowest common 


denominator for Apple’s RISC-based line of machines 
is a 60 MHz PowerPC 601 processor. ‘This change 
represents an enormous jump: on some floating-point 
operations, for example, the Power Macintosh is as 
much as 20,000 times faster. 


In our tests, calculations using the PowerPC processor's single- 
precision floating-point multiply-add instruction were 20,000 times 
faster. This means that if we had started a lengthy floating-point 
calculation in 1984 at the release of the Macintosh, and that 
calculation were still being worked on by the computer, it would 
take a Power Macintosh starting now just four hours to catch up.° 


Even for developers targeting 680x0-based machines, a 
new approach to software design can dramatically 
improve users’ experiences. The goal is to maximize 
use of whatever processing power is available in the 
design of the user interface. 


Here’s a summary of the tips we’d like to pass on; we’ll 
look at each one in more detail below. 


1. Tackle expensive computations when they can 
improve the interface. 


2. Eliminate dialogs and command lines in favor of 
direct manipulation. 


3. Drop old assumptions and idioms. Use the 
processing power to explore new interfaces. 


4. Provide a starting point for exploration. 


5. Avoid programming cleverness. Instead, assume a 
good compiler and write readable code. 


6. Invest development time in user-centered design. 
7. Learn the new rules for performance. 


8. Design tiered functionality: take advantage of 
whatever hardware you’re running on. 


9. ‘Test on real users. 


THE TIPS IN DETAIL 


1. Tackle expensive computations when they can 
improve the interface. 


We took a fresh look at how to implement the visual 
feedback for dragging, scrolling, and zooming. 
‘Traditionally, the Macintosh has represented these with 


GREG ROBBINS began writing educational software on the 
Apple Il as a high school student. Since then, he’s picked up 
computer engineering degrees from U.C. San Diego and the 
University of Pennsylvania, bagged wild boar in Fiji, and evaded 
sharks off Australia. When he’s not consulting on Macintosh 
development, Greg gets way off the beaten track as a member of 
the Bay Area Mountain Rescue Unit.® 


20 develop Issue 19 September 1994 


RON AVITZUR considered working on the Graphing Calculator 
to be an exercise in single-minded obsessive behavior — good 
training for graduate school in physics. Ron has been writing 
educational math software since the dawn of the Macintosh. You 
haven't really seen the Graphing Calculator until Ron has shown it 


to you; in fact, Stewart Alsop’s PC Letter names Ron one of the first 
Demo Gods.® 


XOR animation, which gives the impression of the 
action without really recalculating the window 
contents. This is how we first implemented them in the 
Graphing Calculator as well; redrawing an algebraic 
equation or 2-D curve, not to mention a 3-D rendered 
surface, is computationally expensive. 


But to try to stress the processor, we tested the direct 
approach, and we found that the PowerPC processor 
could easily keep up. So now in the Calculator, when 
the user drags the axes, not only is the image dragged 
live but the exposed parts of the function are calculated 
and drawn as the mouse is moved. When the zoom 
buttons are clicked, the entire function is recomputed 
and redrawn several times to animate the zoom. In this 
way, applications can take advantage of the PowerPC 
processor to dramatically improve the quality of 
interaction in ways that are not possible on slower 
machines. 


2. Eliminate dialogs and command lines in favor of 
direct manipulation. 


Since there’s processing power to burn in the PowerPC 
601, we simplified the user interface by replacing many 
dialogs with direct manipulation. The Graphing 
Calculator doesn’t have the dialogs typical of graphing 
programs for specifying the range and precision of a 
graph (Xniny Xmaxy Ymine Ymaw Number of points, and so 
on). Instead, the user controls the view of a graph 
through direct manipulation. 


‘Today’s computers are fast enough to allow you to 
implement direct interaction for complex tasks. Certain 
time constants play crucial roles in human factors 
analysis. Recognizing these thresholds can help create a 
smoother interface. Ifa task like interacting with an 
equation takes under one-tenth of a second, users 
won't be bothered by the delay; if it takes under one- 
fourth of a second, it’s fast enough not to be annoying. 
Longer delays, however, make users realize that they’re 
waiting. With fast response times, users can ignore 

the computer and have fun exploring the subject at 
hand. 


By emphasizing direct manipulation, we reduced even 
algebraic simplification, a task that might seem to 
require a command-line interface, to the paradigm of 
MacDraw. Math is traditionally intimidating, and math 
programs even more so, but we wanted to make 
mathematics fun. This was really a user-interface 
challenge, and it required rethinking many 
fundamental assumptions. Usability was our primary 
design goal; functionality was second. We were pleased 
to discover that with direct manipulation, we could 
simplify the interface without giving up powerful 
functionality. 


Since direct manipulation doesn’t require users to learn 
any new commands or concepts, the manipulations 
immediately become part of a user’s arsenal of tools. A 
powerful example of this is the drag algebra facility, 
which strikes many people as the most intriguing 
feature of the Calculator. The user can select a term of 
an equation and drag it elsewhere in the equation, just 
like dragging an object in a drawing program. ‘The 
Calculator performs the algebraic manipulations 
necessary to keep the equation consistent. This feature 
immediately boosts users into a realm where they can 
confidently and easily manipulate an equation. Just as 
simple calculators did with multiplication and division, 
it allows users who understand the essential concepts to 
immediately move on to more interesting problems. 


3. Drop old assumptions and idioms. Use the 
processing power to explore new interfaces. 


Ona Power Macintosh, you can handle hundreds or 
thousands of times more information than before 
interactively. This might allow rendered 3-D objects to 
become user-interface components, for example. While 
the Graphing Calculator flashes its buttons for the 
usual 8 ticks to indicate that they’ve been clicked, in 
that time it could compute and render a 3-D surface 
with 1000 polygons. Imagine what controls might look 
like if they really taxed the processing power of the 
machine. 


Because functions are rendered so quickly on a Power 
Macintosh, we made 3-D surfaces spin by default. This 
gives users more information about the functions right 
away. Furthermore, there are no menus or dialogs to 
control the view of surfaces; just as it does for 2-D 
graphs, the Calculator lets users change the 3-D view 
by grabbing the surface with the mouse. 


4. Provide a starting point for exploration. 


Applications should avoid batch setup operations, such 
as requiring users to set a lot of dialog options before 
performing an operation. Instead, provide a starting 
point for exploration, with reasonable defaults for 
whatever’s necessary to get users to that point. 


Perhaps the most unusual aspect of the Graphing 
Calculator is what it doesn’t ask of users. They don’t 
have to set up any graphing options before viewing a 
curve or a surface; there are no preliminary dialogs or 
required commands for users to do this. For any 
equation that can be graphed, the user simply clicks a 
Graph button to draw it. 


We’ve all had the bewildering experience of trying to 


use a program only to discover that we don’t even know 
how to begin. One of the toughest problems is to 


DESIGNING APPLICATIONS FOR THE POWER MACINTOSH 


21 


create an interface that makes functionality available 
and enjoyable for first-time users. But clearly this is 
where the design effort offers the greatest payoff. 


5. Avoid programming cleverness. Instead, assume a 
good compiler and write readable code. 


Cycle-counting and compiler-specific optimizations are 
favorite pastimes of hackers, and sometimes they’re 
important. But we could never have completed the 
Graphing Calculator in under six months had we 
worried about optimizing each routine. Rather, we 
dealt with speed problems only when they were 
perceptible to users. 


We made no attempt to look at performance 
bottlenecks or at the compiled code of the Calculator 
until after running execution profiles. We were 
surprised where the time was being spent. Most of the 
time that the Calculator is compute-bound it’s either in 
the math libraries or in QuickDraw. So little time is 
spent in our code that even compiling it unoptimized 
didn’t slow it down perceptibly. Improving our code’s 
performance meant calling the math libraries less often. 


Programmers are often tempted to spend time saving a 
few bytes or cycles or to fine-tune an algorithm. If the 
change isn’t visible to users, however, the benefits may 
not extend beyond the programmer’s satisfaction. 
When most of the code in an application is 
straightforward and readable, maintenance and 
improvements are easier to make. Those are changes 
that users wil] notice. 


To maximize drawing speed without sacrificing compatibility, 
the Calculator renders its graphs offscreen in GWorlds and uses 
CopyBits to transfer them to the screen. See “Drawing in GWorlds 
for Speed and Versatility” in develop Issue 10 for a discussion of 
this technique. ® 


6. Invest development time in user-centered design. 


Complex algorithms should be used not for their own 
sake but to improve the user experience. For example, 
as the user drags the pane divider in the Calculator 
window, the application redraws most of the window 
(offscreen in a GWorld) rather than recalculating 
exposed areas. The Power Macintosh is fast enough 
that it wasn’t worth spending coding and debugging 
time to save on runtime calculation, because the savings 
wouldn't be perceived by the user. 


In contrast, when users click on a 2-D curve to read an 
(x,y) coordinate, some quite sophisticated processing 
happens. As the user moves the mouse, a numeric root- 
finding algorithm looks for interesting points such as 
maxima, minima, and zero-crossings to solve equations 


22 develop Issue 19 September 1994 


numerically. Furthermore, because numerical methods 
to find maxima and minima are imprecise, we also 
compute symbolic derivatives of the functions and then 
look for where the derivative is 0, which locates 
maxima and minima much more accurately. 


All this work goes completely unnoticed by the user. 
But the user does notice the result: simply clicking on 
the curve tells with great precision what's interesting at 
that point. 


7. Learn the new rules for performance. 


You may discover that there are places where 
performance tuning would be worthwhile in your 
application. The rules for performance have changed, 
and knowing the new rules is essential. Some 
programming techniques that traditionally improve 
performance can be counterproductive. In particular, 
on PowerPC-based systems, avoiding instruction cache 
misses is far more important than saving instructions. 


Getting good performance out of a fast machine 
doesn’t always come without effort. To be able to 
exploit modern hardware to improve an application, 
you must have some understanding of the hardware 
and what allows it to be fast. It’s extremely important to 
understand the processor and memory architecture of 
your target platform. 


‘The memory system in the Power Macintosh is much 
faster than the memory system of other Macintosh 
models. The 64-bit bus allows for substantially 
improved data transfer. However, the processor is 
much, much faster than the memory system. An 
uncached memory reference may take 20 times as long 
as a cached memory reference. Performance will 
actually be slowed down dramatically by a speed 
optimization that saves floating-point multiply 
instructions (expensive on some processors, but not on 
the PowerPC) at the expense of extra memory usage 
that forces instructions or data out of the cache. 


Understanding patterns of memory reference is very 
important in analyzing algorithms for performance. 
Stepping through an array across cache lines can 
quickly flush all lines out of the cache. (Cache lines are 
discussed in the Balance of Power column in this issue.) 
This can cripple attempts to walk the data structures 
typically maintained by interface-intensive applications. 
The PowerPC 601 has an eight-way set associative 
cache, which is fairly resilient to degradation from 
flushing of cache lines. However, the 603 processor has 
just a two-way set associative cache. Any processor- 
intensive calculations must avoid cache thrashing if 
they are to avoid degrading below an acceptable level of 
user responsiveness. 


For additional architecture insights and tuning strategies, 
see the Balance of Power column in develop Issue 18 and in this 


issue. ° 


Because no two platforms will run at the same speed, 
it’s important to design software to work well on a 
variety of machines. In principle, this means that the 
same application could run on any Macintosh, while 
being far more powerful — and more pleasant to use — 
on a Power Macintosh. 


8. Design tiered functionality: take advantage of 
whatever hardware you’re running on. 


Just as it’s frustrating for users of entry-level machines 
to be unable to run software, it’s equally frustrating for 
users of fast, high-end hardware with plenty of memory 
to have features execute unnecessarily slowly, or to be 
constrained by programs that expect and only take 
advantage of minimal resources. 


The Graphing Calculator does assume a Power 
Macintosh as its base platform; otherwise its 
expectations are modest. However, its appetite is 
unbounded. It does all drawing using offscreen buffers 
in temporary memory when adequate RAM is available, 
but will scale back to onscreen, QuickDraw-based 
rendering when temporary memory is limited. System 
7 makes this easy with purgeable GWorlds; once 
LockPixels fails, the Calculator knows that it must 
work within tighter constraints. Later, when it can 
reallocate the offscreen buffer, it does so and resumes 
the fast, smooth graphing effects. When memory is 
abundant, the Calculator uses many temporary 
GWorlds to buffer frames of 2-D inequality graphs 
calculated for varying values of, such as the animation 
of cos nx < 0. 


‘The Graphing Calculator does all 2-D graph 
computation in 15-tick chunks. For simple curves, this 
typically renders the entire curve at once. But 2-D 
inequalities are drawn piece by piece. Once Power 
Macintosh computers are fast enough that an entire, 
complex inequality graph can be drawn in a single 15- 
tick time slice, users will be able to explore inequalities 
as fluidly as they can now play with simple functions. 


‘To take advantage of faster machines, always base 
computational units on real time rather than on more 
arbitrary measures. If the Calculator had recomputed a 
fixed number of points and then called WaitNextEvent, 
too much time would have been yielded to other 
processes, even on graphs simple enough to be 


Thanks to Arnaud Gourdol, Mike Neil, Don Norman, and Alex 
Rosenberg for reviewing this column. ° 


recomputed and drawn all at once. Instead, the 
Calculator calls TickCount and lets that dictate when it 
needs to yield time. This approach allows for a smooth 
user interface and cooperative execution regardless of 
the processor’s speed. 


Designing tiered functionality means abandoning 
assumptions about what is and is not practical on the 
target hardware. For addressing resources, such as 
available hardware and memory, or when execution can 
be threaded or time-sliced, the options are usually 
clear. For matters of raw speed, the proper approach 
may not be so obvious. It may come down to measuring 
the execution speed of a particular procedure at run 
time and basing a decision on that. For example, an 
animation effect might be suitable if it takes under 8 
ticks, or a routine that bypasses QuickDraw for 
drawing in a GWorld may be worthwhile if it really is 
faster than its QuickDraw alternative. 


For an example of timing graphics speed, see the article 
“Exploiting Graphics Speed on the Power Macintosh” in develop 
Issue 18.° 


9. Test on real users. 


The Graphing Calculator reflects our vision of a new 
kind of calculator. But user testing was essential for 
showing us the holes in our vision. For example, 
elements that were directly manipulable in our eyes 
were overlooked by users. So we redesigned them to 
make their functions clearer, and then added a demo 
mode to point out important controls explicitly. 


In tests, the users who looked at the help pages 
invariably turned first to the page at the end offering 
“tips.” This surprised us, but also made clear where to 
put the most important information for using the 
Calculator. 


‘Toward the end of the development of the Graphing 
Calculator, as the final user tests were being conducted, 
we saw students using the Calculator effectively within 
minutes, without instruction. Watching a high school 
student say “Wow!” as an equation came to life on the 
screen was probably the most satisfying moment for us 
as programmers. It was also the one that offered us the 
greatest hope about the future of personal computing. 
‘The Power Macintosh offers us the chance to reach 
more people while making the experience more 
enjoyable and easier for users than ever possible on 
earlier generations of computers. As developers, we 
have a new world to explore. 


DESIGNING APPLICATIONS FOR THE POWER MACINTOSH 23 


Adding QuickDraw GX Printing to QuickDraw 
Applications 


Now that QuickDraw GX has been released, you may be wondering 
what to do with your older QuickDraw applications. The good news ts 
that by adding a relatively small percentage of code to your existing 
applications, you can support all the new features of the QuickDraw 
GX printing architecture and still retain full compatibility with 
non—QuickDraw GX systems. 


Compatibility with the existing application base was a primary concern during the 
development of QuickDraw GX; only the most hardened “print criminals” should have 
compatibility problems. Since a pre-QuickDraw GX application should work in both 
QuickDraw GX and non—QuickDraw GX environments, you may think that leaving 
your code as it stands is good enough. Becoming compatible probably doesn’t require 
any revisions to your existing software, and the fact that your application will perform 
with or without QuickDraw GX sounds like a pretty good deal. Where’s the catch? 


Here it is: A non—QuickDraw GX application doesn’t have access to several key 
features of QuickDraw GX, so its users can’t take advantage of many of the new 
printing features. For example, with QuickDraw GX a user can: 


¢ Redirect print jobs using the new Print dialog. 


DAVE HERSEY 


* Choose page-by-page formatting, so that page | prints on US 
Letter, page 2 prints on #10 envelope, page 3 prints on landscape- 
oriented US Legal, and so on, instead of printing the entire 
document in a single format. 


¢ Work with the new QuickDraw GX printing dialogs (shown in 
Figure 1) instead of the dialogs provided for compatibility with 
non—QuickDraw GX applications. 


¢ Use features provided by printing extensions. Printing extensions 
can add items to the QuickDraw GX printing dialogs, but not to 
the compatibility dialogs. (Added items show up as icons in the 
column on the left side of each dialog.) 


¢ Print a single copy of a document with the new Print One Copy 
command. 


¢ Print documents by dragging them to desktop printers. 


DAVE HERSEY likes the annoying buzzing noise Apple Color Plotter (which is from an era before 


that ImageWriters and ImageWriter LQs make. the first Macintosh). When asked why he 
He also enjoys listening to Guns N Roses, but bothered, he said “Well, it makes this cool 
only while he’s answering developer e-mail at wacka-wacka sound.” Clearly, Dave is an 


Apple's Developer Support Center. Dave recently audiophile for the ‘90s.° 
wrote a QuickDraw GX printer driver for the 


24 develop Issue 19 September 1994 


Page Setup Custom Page Setup 


Paper Type: US Letter 


Orientation: tie} 
Scale: te 


Paper Type: US Letter 


Orientation: tia} 
Scale: % 


Fewer Choices Remove Cancel 


Page Setup dialog Custom Page Setup dialog 


Print 


Print to: Clasmo Yasmo bd 


Pages: @ All 
O From: | | To: | 


Copies: |1 (J Collate Copies 


Paper Feed: @ Automatic 
© Manual 


Destination: [Printer v] Quality: 


Print dialog 
Figure 1. The new QuickDraw GX printing dialogs 


Users are shut out from all these features if they’re using a non—QuickDraw GX 
application. In this article, you’ll see how to increase an application’s level of 
QuickDraw GX support. I’ll take a QuickDraw application and convert it step by step 
into an application that fully supports both the QuickDraw and QuickDraw GX 
printing architectures. First, we'll consider some definitions and background 
information. 


DIFFERENT LEVELS OF QUICKDRAW GX SUPPORT 


To begin, let’s define the different levels of QuickDraw GX support (from lowest to 
highest) that an application can have: 


© QuickDraw GX unaware: The application doesn’t implement any 
QuickDraw GX code, and QuickDraw GX translates all 
QuickDraw printing and drawing calls into QuickDraw GX 
equivalents. In other words, this is a non—QuickDraw GX 
application. 


© QuickDraw GX aware: The application implements a core set of 
QuickDraw GX printing features but retains compatibility with 
documents created with earlier versions. 


© QuickDraw GX savvy: The application implements full support for 
QuickDraw GX graphics, typography, and printing features. It 
also retains compatibility with documents created with earlier 
versions. 


© QuickDraw GX dependent: The application requires the presence of 
QuickDraw GX graphics, typography, and printing routines to 
function. 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


25 


26 develop Issue 19 September 1994 


Applications don’t need to implement any code to be QuickDraw GX unaware. 
Becoming QuickDraw GX aware requires some coding, but typically not very much. 
Making your application QuickDraw GX aware is the minimal level of QuickDraw 
GX support that you should set your sights on — just follow the guidelines in this 
article. 


A QuickDraw GX-aware application is really a non—QuickDraw GX application at 
heart, with support for QuickDraw GX printing added. This application still uses its 
old QuickDraw commands to represent shapes and text; however, in its print loop, 
the application uses the QuickDraw GX graphics translator to translate QuickDraw 
commands into QuickDraw GX shapes for printing, if QuickDraw GX is available. 


If you’re just starting out on a new project, or you’re in the process of rewriting an 
existing product, you should consider the next level of compatibility: QuickDraw GX 
savvy. A QuickDraw GX-savvy application meets the requirements for being 
QuickDraw GX aware but also takes advantage of the extensive QuickDraw GX 
graphics and typography tools. You may feel that becoming QuickDraw GX savvy, 
although it brings with it a wealth of increased capabilities, is too big a leap to take in 
the short term. In that case, consider making your existing applications QuickDraw 
GX aware and, once they’re released, going back and working on QuickDraw 
GX-savvy versions. 


A WORD ABOUT COMPATIBILITY 


It’s Apple’s hope and goal that no QuickDraw GX-unaware applications will be 
incompatible with QuickDraw GX. An existing application can be incompatible only 
if it relies on unsupported methods or on unpublished information that breaks under 
QuickDraw GX. For printing, this would include things such as trying to access the 
Printer Access Protocol (PAP) code from the 'PDEF' 10 resource of the LaserWriter 
driver. The QuickDraw GX LaserWriter driver doesn’t contain a 'PDEF' 10 
resource, so applications that rely on this approach must be modified to work with 
QuickDraw GX. 


Developers were warned about the disappearance of the 'PDEF' 10 resource in 
“Print Hints: Looking Ahead to QuickDraw GX” in develop Issue 13.° 


If your application has compatibility problems with QuickDraw GX, you probably 
already suspect it. You’re a candidate for such problems if you’re perpetrating any of 
the “printing crimes” or shaky methods that Apple has warned about in the past. 
Sweaty palms and pangs of guilt whenever your application is tested under new 
system software are likely indications that you should get out your development tools 
and reform your code now. (If you need the old code to run on non—QuickDraw GX 
systems, simply provide alternative code for running with QuickDraw GX.) Delve 
into the QuickDraw GX API — you're sure to find a better way of doing things. 


SOME FUNDAMENTALS 


Before we go into the specifics of how to add QuickDraw GX printing to your 
existing applications, we need to cover some fundamental ideas. I'll start with a 
discussion of the Collection Manager, which makes its debut with QuickDraw GX, 
and then briefly cover overriding messages and the Message Manager. 


For a review of QuickDraw GX printing, see “Getting Started With 
QuickDraw GX" and “Developing QuickDraw GX Printing Extensions,” both in 
develop Issue 15. See also Inside Macintosh: QuickDraw GX Printing.° 


CREATING A FOUNDATION TO BUILD ON: THE COLLECTION MANAGER 

The Collection Manager is somewhat like a memory-based version of the Resource 
Manager. It provides API calls that allow you to create and access collections, which are 
amorphous structures that can contain data of different types with different sizes. 
Collections are similar to resource files under the Resource Manager except that 
collections must be in memory, while resource files are by definition disk based. 
Routines exist for “flattening” collections into a series of bytes that can be written to 
disk and for “unflattening” them for later use. 


Just as a resource file may contain Apple-defined data structures such as 'WIND' and 
‘MENU ' resources, a collection may contain predefined collection items such as 
‘copy’ and 'rang’, which specify the number of copies of a print job and the page 
range of a print job, respectively. Like resources, collection items have attributes that 
are predefined (for example, the collectionLockMask attribute), but unlike resources, 
they also have attributes that can be programmer defined. And, as with resources, you 
aren’t simply stuck with the core set that’s been defined by Apple. Figure 2 illustrates 
the similarities, including a programmer-defined resource of type 'USER' and a 
programmer-defined collection item, also of type 'USER'. 


Resource type: 'USER' “] Collection |-*” 


Resource ID: 128 a ‘USER! | 
Resource attributes: ‘ ‘copy' ; ! 

locked . copy" | . 
Variable-length data... . "rang' | 


Resources on hard disk Collection items in memory 


Collection item type: 'USER' 


Collection item ID: 10 


Collection item attributes: 
collectionLockMask 


Variable-length data... 


Figure 2. Similarities between resources and collection items 


Items in a collection are uniquely identified in three different ways. Every item has a 
4-character label, or tag, which identifies the type of collection item. In Figure 2, 
there are three collection items having the tags 'USER’, ‘copy’, and ‘rang’, 
respectively. In addition to the tag, every collection item has a longword ID. 
‘Together, the tag and ID are one way to uniquely identify a collection item. Each 
collection item can be also be referenced by its collection index or by its tag and tag 
list position. The index is the relative position of an item in a collection, and the tag list 
position is the relative position of a collection item within the item’s tag type — for 
example, the first, second, or third 'USER' item in a collection. This gives us three 
ways to refer to a specific collection item: 


¢ by its tag and ID 
¢ by its collection index 
° by its tag and tag list position 
It’s important to note that although a collection item’s index and tag list position may 


change as items are added or removed from a collection, an item’s tag and ID will 


never change. Therefore, the most common way to refer to collection items is by tag 
and ID. 


Collection tag and resource type naming conventions are the same. Apple 
reserves tags listed in the printing interface files, as well as those consisting entirely of 
lowercase letters. ° 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


27 


238 develop (Issue 19 September 1994 


You can either create a collection with the NewCollection routine or use one of the 
three types of collections that QuickDraw GX automatically creates for you during 
printing. These types are job collections, format collections, and paper-type 
collections. Every time a new gxJob, gxFormat, or gxPaperType object is created, a 
collection is also created for that object. These collections contain items specifying 
the following types of information: 


¢ job collection: number of copies, page range, destination file 
information (if printing to a file) 


¢ format collection: scaling factor, page orientation, page flipping 
information 


* paper-type collection: paper-type creator, paper-type comment 


These predefined collections contain many more collection items than are shown 
here. See the QuickDraw GX interface file PrintingManager.h for the complete 
listing. By simply changing the contents of the collection items, any application, 
printing extension, or printer driver can easily affect how a document will be printed. 


Note that all predefined collection items have the ID gxPrintinglagID = 
-28672.° 


Your application can create and use collections for its own purposes. By no means are 
you restricted to using collections only within the QuickDraw GX printing 
architecture, although that’s probably where you'll use them the most. 


OVERRIDING MESSAGES FROM AN APPLICATION: THE MESSAGE MANAGER 
Sam Weiss’s article in develop Issue 15, “Developing QuickDraw GX Printing 
Extensions,” introduced the Message Manager. The QuickDraw GX printing 
architecture uses the Message Manager to invoke printing operations, and your 
application, printing extension, or printer driver can override messages to change 
printing behavior. This process is explained in Sam’s article, which is a good reference 
for anyone unfamiliar with the Message Manager and how QuickDraw GX uses it. 
This article assumes that you already have at least a passing familiarity with the basic 
concepts. 


When you override a message from an application, you’re more restricted in what 
you can do than if you override a message from a printing extension or printer driver. 
‘To understand why, consider a case where you want to override the QuickDraw GX 
printing message GXDespoolPage to add a serial number to the page before it’s 
printed. You can add this override to a printing extension in a straightforward way 
that works regardless of which application prints the document. If, on the other hand, 
the override is attempted by an application, the situation becomes more problematic. 
The override won’t be invoked because it’s a despooling-phase message; only 
application-phase and spooling-phase messages can be overridden by applications. 


‘To understand a second limitation of application overrides, let’s look at how they’re 
installed in the message handler chain. This is done by passing the routine 
GXInstallApplicationOverride a pointer to the override procedure and a reference 
to the document’s job, like so: 


GXInstallApplicationOverride(docJob, gxJobPrintDialog, 
MyJobPrintDialogOverride) ; 


In this example, the application is overriding the GXJobPrintDialog message with an 
override function called MyJobPrintDialogOverride. docJob is the gxJob object for a 


document. Because your application has access only to its own gxJob objects, an 
application override can be invoked only for the application that installed it. On the 
other hand, a message override from a printing extension can affect every document 
that’s printed, regardless of which application printed it. 


In summary: 


¢ Application overrides are reliable only during the application and 
spooling phases of printing, which end once the document has 
been spooled to a print file. You should attempt to override only 
application-phase and spooling-phase messages from an 
application. 


¢ Application overrides are invoked only for the application 
installing the override, and only for the gxJob objects in which the 
overrides are installed. 


Application overrides are best suited for adding features to the printing dialogs and 
modifying the print file as a document is being spooled. If you feel limited by either 
of the conditions mentioned above, you should move your override out of your 
application and into a printing extension. 


BECOMING AWARE 


Now let’s look at what an application developer needs to do to support QuickDraw 
GX printing. We’ll take a QuickDraw application called Simple Sample and convert 
it into its QuickDraw GX-aware counterpart, Simple Sample GX. The code for both 
samples is on this issue’s CD. 


The Simple Sample application draws using various QuickDraw commands, 
including those for simple objects, bitmaps, and PicComments. It can handle 
multiple documents with multiple pages, and although it knows nothing about 
QuickDraw GX, it is System 7 dependent (I made it that way to save on code and 
confusion). 


Here’s a summary of what most QuickDraw GX~-aware applications need to do: 


¢ Determine whether QuickDraw GX is present, and if so, initialize 
the QuickDraw GX managers (and close them down when the 
application quits). 


* Create a gxJob object for each document created and dispose of 
the gxJob when the document is closed. 


* Override the GXPrintingEvent message (in order to support the 
QuickDraw GX movable modal printing dialogs) while printing 
dialogs are displayed. 


¢ Update gxJob objects on resume events (in case the characteristics 
of the desktop printer you’re printing to have changed). 


¢ Save and load a document’s gxJob object to and from disk and 
convert print records to gxJob objects when loading documents 
created from pre-QuickDraw GX versions of your application. 


¢ Make the Custom Page Setup and Print One Copy menu items 
available in the File menu. 


¢ Invoke the QuickDraw GX printing dialogs and support custom 
formatting by page (each page can have a unique page format, or 
can share another page’s format). 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


29 


30 develop (Issue 19 September 1994 


¢ Store page-to-format correspondences to disk with a document 
(and load them again when the document is opened). 


¢ Have a print loop that uses QuickDraw GX printing commands 
and translates QuickDraw commands to QuickDraw GX shapes 
for printing. 


¢ Implement the Print One Copy feature. 


¢ Support the new attribute of the 'pdoc' Apple event to properly 
support drag-and-drop printing of documents from the Finder. 


Each of these is discussed in order in the rest of this article. 


PREPARATION 

Before you can do anything with QuickDraw GX, you need to determine whether it’s 
available. You should do this in your initialize routine, after you’ve determined that 
the other managers your application requires are available. The MyInitGXI{Present 
routine in Listing 1 shows one way of doing this. 


Listing 1. QuickDraw GX preparation and cleanup 


void MyInitGxIfPresent() 


{ 
long gxVersion, gxPrintVersion; 
gGXIsPresent = false; 
/* Check to see whether QuickDraw GX is available. */ 
if (Gestalt(gestaltGXVersion, &gxVersion) == noErr) 
if (Gestalt(gestaltGxPrintingMgrVersion, &gxPrintVersion) 
== noErr) 
gGXIsPresent = true; 
/* If so, initialize QuickDraw GX. */ 
if (gGXIsPresent) { 
gClient = GXNewGraphicsClient(nil, kGraphicsHeapSize, 
(gxClientAttribute) 0); 
GXEnterGraphics(); 
GXInitPrinting(); 
} 
} 


void MyCleanUpGxIfPresent() 


{ 
if (gGXIsPresent) { 
GXExitPrinting(); 
GXDisposeGraphicsClient(gClient) ; 
GXExitGraphics(); 
} 
} 


In MyInitGXIfPresent, we use Gestalt to determine whether the QuickDraw GX 
graphics and printing routines are present. If so, we call GX NewGraphicsClient 

to set aside memory for QuickDraw GX graphics operations, and we call 
GXEnterGraphics and GXInitPrinting to enable the QuickDraw GX functions we’ll 


use. Note that we also set a global variable, gGXIsPresent, which indicates whether 
the QuickDraw GX managers we require are present. We’ll check this value 
whenever we need to make a decision about whether to use QuickDraw or 
QuickDraw GX methods. 


Important: You cannot intermix Printing Manager and QuickDraw GX printing calls. 
Do not call _PrGlue[A8FD] if you have called _InitPrinting. 


When the user quits the application, we need to release any memory we allocated and 
close down QuickDraw GX printing and graphics. We do this as shown in the 
MyCleanUpGXIfPresent routine in Listing 1. 


CREATING AND DISPOSING OF GXJOB OBJECTS 

With the MyInitGXIfPresent and MyCleanUpGXIfPresent routines added to our 
code, we’re now ready to make our application’s documents fit into the QuickDraw 
GX print model. We do this by creating a gxJob object whenever we create a 
document. Our non—QuickDraw GX application, Simple Sample, contains a routine 
named MyCreateDocument that’s called whenever the user creates a document. As 
shown in the Simple Sample GX application, this routine is modified so that when 
QuickDraw GX is present (as indicated by the gGXIsPresent global variable), the 
application creates a gxJob for each document, using the GXNewJob routine. If 


QuickDraw GX is not present, the application creates a print record handle 
(THPrint) instead. 


It’s common for an application to store descriptive information about a document in a 
private data structure, and our sample is no exception. The application uses a private 
structure called MyDocumentRec that contains information about the number of 
pages in a document, the current page being viewed, and so forth. We modify this 
structure also, so that we can store a document’s job reference and page formatting 
information with the document. As seen in Listing 2, we’ve added the document] ob 
and pageFormat fields to the structure. The rest of the fields in this structure were 
already being managed by the QuickDraw GX-unaware application, and we'll 
continue to use them. 


Listing 2. MyDocumentRec, modified for QuickDraw GX 


#define kMaxPages 20 /* Max pages the sample handles. */ 
typedef struct MyDocumentRec { 
THPrint documentPrintHdl; /* Print record for document. */ 
gxJob documentJob; /* Job for document. */ 
gxFormat pageFormat[kMaxPages]; /* Format for each page. */ 
/* If nil, we use the job format. */ 


long numPages; /* Number of pages in document. */ 
long curPage; /* The current page displayed. */ 
FSSpec documentFSSpec; /* Document's file spec. */ 

Str31 documentTitle; /* The title of this document. */ 


WindowPtr documentWindow; /* The window for this document.*/ 


} MyDocumentRec, *MyDocumentPtr; 


We also need to modify the application’s MyDisposeDocument routine, which is 
called whenever a document is closed. In the modified routine, we dispose of the 
document’s gxJob. 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 31 


32 develop Issue 19 September 1994 


The changes that we’ve made so far are indicative of the approach we’ll continue to 
use as we make our sample QuickDraw GX aware: adding code that executes only if 
QuickDraw GX is present. If the user isn’t running QuickDraw GX, our converted 
sample will appear and function as the original did. But if QuickDraw GX is present, 
all the new functionality will kick in. 


OVERRIDING GXPRINTINGEVENT 
When we added support for gxJobs in the MyCreateDocument routine, we also 
added the following line of code: 


GXInstallApplicationOverride((*createdDocument )->documentJob, 
gxPrintingEvent, MyPrintingEventOverride) ; 


Every application that aspires to be QuickDraw GX aware should include such a 
line. Remember, QuickDraw GX has movable modal printing dialogs; the 
GXPrintingEvent message is sent whenever a dialog is moved. Unless you override 
GXPrintingEvent, you won’t have a chance to update your application’s windows 
when the dialogs are moved. 


The code to support window updates when the printing dialogs are moved is actually 
quite simple. You just need to add a routine that overrides the GXPrintingEvent 
message and calls your event-handling routine (Listing 3), and to make sure that 
you’ve disabled the appropriate menu items before displaying the new printing 
dialogs. Note that your override should not forward the GXPrintingEvent message, 
but instead should perform a total override of it. 


Listing 3. MyPrintingEventOverride 


OSErr MyPrintingEventOverride(EventRecord *anEvent, Boolean filterEvent) 
{ 

/* Handle events in whatever way is appropriate. MyDoEvent is 
our generic event handler. We don't pass it events that it 
shouldn't handle while printing dialogs are displayed. */ 

if (!filterEvent) 
switch (anEvent->what) { 

case mouseDown: 
case keyDown: 
case autokey: 
break; 
default: 
MyDoEvent(anEvent) ; 
} 


return noErr; 


UPDATING GXJOB OBJECTS ON RESUME EVENTS 

There’s another piece of event-handling code that every QuickDraw GX-aware 
application should include. To support the reentrant nature of QuickDraw GX, you 
must call GXUpdateJob on each gxJob that your application is using whenever you 
receive a resume event. (See the code for Simple Sample GX on the CD.) This 
enables QuickDraw GX to update the information in the gxJob in case it changed 
while your application was in the background. As an example, suppose that a user 
suspends your application to change the printing extension setup for the destination 


desktop printer. Upon switching back to your application, this user will expect any 
open documents to use these new settings at print time. Unless you call GX UpdateJob 
on your gxJob objects, the new settings won’t be there. 


SAVING AND LOADING A DOCUMENT’S GXJOB 

Now that we can create gxJob objects for new documents, let’s take a look at how we 
can save these to disk and later retrieve them when the documents are opened. We 
want to save them for the same reason that we want to store print records with 
documents under non—QuickDraw GX systems: if a user has gone to the trouble of 
configuring print settings for a document, the user-friendly thing to do is to use those 
settings the next time the document is opened. 


Because the data we want to save is stored in an abstract data structure (gxJob), we 
need a way to convert it to a more tangible form. We accomplish this with the 
GXFlattenJob or GXFlattenJobToHdl routine. GXFlattenJob passes the converted 
data as a stream of bytes, whereas GXFlattenJobToHd1 places the flattened data in a 
handle. You would typically use GXFlattenJob to store the flattened gxJob in a data 
fork, but GXFlattenJobToHd1] to save it as a resource. 


Since our application stores its print records in resources, we'll store its flattened 
gxJob objects in resources as well. To do this, we modify our application’s 
MySavePrintInfo routine, which is called to save print records to disk. If QuickDraw 
GX is present, the routine will instead save our flattened gxJob. 


In the following code, we flatten a gxJob into a handle called thePrintData, which can 
then be written to the disk as a resource. 


thePrintData = NewHandle(0); 
GXFlattenJobToHd1(whichDocument->documentJob, thePrintData); 


Next, we modify the application’s MyLoadPrintInfo routine, which is used to retrieve 
print records that were previously saved with MySavePrintInfo. This routine must do 
several things, based on whether QuickDraw GX is present and whether a gxJob or 
print record has been previously stored with a document. The flow of control is 
shown in Listing 4. 


Listing 4. MyLoadPrintinfo flow of control 


if (gGXIsPresent) { 
if there’s a previously saved gxJob 
unflatten it and use it 
else 
if there’s a previously saved print record 
convert that to a gxJob and use it 
else 
use the default gxJob we created in MyCreateDocument 


} 
else { 
if there’s a previously saved print record 
use it 
else 
use the default print record we created in MyCreateDocument 
} 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


33 


34 develop (Issue 19 September 1994 


As it turns out, some of the steps in Listing 4 can be combined. For example, 
regardless of whether QuickDraw GX is present, we may need to retrieve a 
previously saved print record. What?! Accessing old-style print records when 
QuickDraw GX is present? Sounds strange, doesn’t it? We need to do this if 
QuickDraw GX is available and the user opens a document that contains a print 
record but not a gxJob (because it was created with an older version of the 
application). We convert the print record to a gxJob with the GXConvertPrintRecord 
routine, so that the gxJob has as many of the old print record’s settings as possible. 


CONFIGURING AND HANDLING MENUS 

We need to alter our menu routines so that the Custom Page Setup and Print One 
Copy commands are available to QuickDraw GX users. We might decide to have two 
different File menus, the installation of which would depend on whether QuickDraw 
GX is present. Or we might have only one File menu and add or subtract items from 
it when it’s installed in the menu bar. The approach that’s best for a particular 
application depends on how the application is structured. (The same choice of 


approaches applies to other menus that might need to be modified based on whether 
QuickDraw GX is available.) 


In the sample code, we modify the File menu as follows: the application contains a 
‘MENU ' resource for the QuickDraw GX version of the File menu; if gGXIsPresent 
is false, we remove the Print One Copy and Custom Page Setup menu items from 
this menu. 


fileMenu = GetMHandle(mFile); 
DelMenuItem(fileMenu, iPrintOneCopy) ; 
DelMenuItem(fileMenu, iCustomPageSetup) ; 


The order of the deletions is very important! If we deleted in the reverse order, the 
Custom Page Setup menu item would be deleted, but when we tried to delete Print 
One Copy, we would actually delete whatever came after Print One Copy. Why? 
Because the menu would be one item shorter, and the index into it would no longer 
be valid. Just delete menu items from bottom to top and you'll be fine. 


Monkeying around with the placement (and inclusion) of menu items like this will 
throw our menu-enabling and menu-selection routines out of whack. We need to 
make sure that regardless of whether the Print command is item number 8 or 9 in the 
menu, we still treat it as a Print command. Again, the approach to use will depend on 
the application. 


It’s conceivable that you would use macros and would make the same changes to both 
the menu-enabling and menu-selection routines, but in the sample I chose to tackle 
menu enabling and disabling differently than I did menu handling. For the simple 
enabling and disabling of menus in the MyAdjustMenus routine, I check to see 
whether QuickDraw GX is present and adjust the item numbers based on that. This 
is the easiest approach because it requires only minor changes to the routine. 


‘The menu selection situation is a little different. Typically, applications contain a 
routine that uses a switch statement based on the ID of the menu and menu items 
chosen. This means that we’d need either two such routines (one for QuickDraw 
and one for QuickDraw GX) or a different approach from what we used in 
MyAdjustMenus. I opted for a different approach. 


The sample application’s MyDoMenuCommand routine uses a switch statement (as 
most applications do) based on the menu ID and menu item extracted from the result 
of the MenuSelect and MenuKey routines. When QuickDraw GX is not present, the 


Quit menu item will be item 10 in the File menu; otherwise, it will be at item 12 (see 
Figure 3). The switch statement in the MyDoMenuCommand function compares the 
menu item selected to the items in the QuickDraw GX version of our File menu. 
Therefore, it will expect that item 12 will be Quit. Item 10 will be interpreted as 
Print One Copy! This would be disastrous — the application would not quit, and our 
QuickDraw GX-specific code would be executed when QuickDraw GX wasn’t 
around! That’s not likely to be a pleasant user experience. 


New New 
Open... #0 Open... #0 
Close #W Close sew 
Save #S Save #S 
Save As... Save As... 
Page Setup... Page Setup... 
Print... 36P Custom Page Setup... 
Print... 36P 
Item 10 ——-~ Quit 3 Print One Copy 
Without QuickDraw GX eerie Quit 9 0 


When QuickDraw Gx is present 


Figure 3. The File menu without QuickDraw GX and with QuickDraw GX 


A new routine, MyConvertMenultem, solves this problem (Listing 5). This routine is 
called just before we enter the switch statement in MyDoMenuCommand. If 
QuickDraw GX is present, MyConvertMenultem does nothing; otherwise, it checks 
to see if the menu item selected was affected by our deletion of the QuickDraw GX 
menu items, and if so adjusts it. How’s that for an easy solution? 


Listing 5. MyConvertMenultem 


void MyConvertMenuItem(short *menuID, short *menuItem) 


{ 
if (!gGXIsPresent) { 
if (*menuItem == iCustomPageSetup) 
*menultem = iPrint; /* Print was selected. */ 
else 
if (*menuItem == iPrintOneCopy) 
*menuItem = iQuit; /* Quit was selected. */ 
} 
} 


INVOKING THE PRINTING DIALOGS AND SUPPORTING CUSTOM 
FORMATTING 

Earlier, we added code to our application to support window updates when the 
printing dialogs are displayed. Now, let’s discuss the code required to actually display 
the dialogs. 


There are now three printing dialogs instead of two (as shown earlier in Figure 1). 
The Page Setup dialog now allows users to modify a document’s default page format. 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


35 


The new dialog, Custom Page Setup, provides a way to change page formatting on a 
page-by-page basis. If your application creates documents that can have only a single 
page, implementing the Custom Page Setup dialog isn’t necessary; the Page Setup 
dialog can be used to configure the document’s only page format. The Print dialog is 
similar to its non—QuickDraw GX counterpart, although much enhanced. 


We use GXJobDefaultFormatDialog, GXFormatDialog, and GXJobPrintDialog to 
display the Page Setup, Custom Page Setup, and Print dialogs, respectively. 


We modify our MyDoPageSetup routine to call GXJobDefaultFormatDialog instead 
of PrStlDialog when QuickDraw GX is present. In our MyDoCustomPageSetup 
routine, which is called only when QuickDraw GX is available, we simply call 
GXFormatDialog, passing the current page’s gxFormat. 


In both of these page setup routines, and in the code for MyPrintDocument that 
follows, we call a function named MyAdjustMenusForPrintDialogs, which disables 
and enables entire menus (Listing 6). We disable menus before displaying a printing 
dialog, and enable them once the dialog goes away. If we didn’t disable the menus, 
users would be able to select menu items. And, because the GXPrintingEvent 
override calls our MyDoEvent routine, any queued-up menu selections would be 
processed when the GXPrintingEvent message was sent. The user could be in the 
Print dialog, then select Quit from the File menu, and the next time the window was 
updated, the application would quit! The only menus a user should have access to are 
the Edit menu and the system menus. (Users can open desk accessories from the 
Apple menu, for example, while a printing dialog is displayed — but they should not 
be able to open the About item for the application.) 


Listing 6. MyAdjustMenusForPrintDialogs 


void MyAdjustMenusForPrintDialogs(Boolean dialogGoingUp) 
{ 


MenuHandle appleMenu, fileMenu, editMenu, documentMenu; 


appleMenu = GetMHandle(mApple); 

fileMenu = GetMHandle(mFile); 

editMenu = GetMHandle(mEdit) ; 

documentMenu = GetMHandle(mDocument) ; 

if (dialogGoingUp) { 
DisableItem(appleMenu, iAbout); 
DisableItem(fileMenu, 0); 
DisableItem(documentMenu, 0); 
HiliteMenu(0); 

} 

else { 
EnableItem(appleMenu, iAbout) ; 
EnableItem(fileMenu, 0); 
DisableItem(editMenu, 0); 
EnableItem(documentMenu, 0); 

} 

DrawMenuBar (); 

gInPrintDialog = dialogGoingUp; 


36 develop (Issue 19 September 1994 


The system enables some menus, namely the Help menu, the Application 
menu, and the Keyboard menu, even when dialogs are displayed. Your code doesn’t 
need to deal with them. ° 


‘The MyPrintDocument routine (Listing 7) displays the appropriate Print dialog and 
then branches to our QuickDraw or QuickDraw GX printing routine. Note that 
we've been careful to call PrOpen and PrClose only when QuickDraw GX is not 
present. As mentioned earlier, you cannot intermix Printing Manager and 
QuickDraw GX printing calls. 


Listing 7. MyPrintDocument 


OSErr MyPrintDocument (MyDocumentPtr whichDocument ) 
{ 

OSErr err = noErr; 

gxEditMenuRecord editMenuRec; 

gxDialogResult result; 


if (gGXIsPresent) { 

/* If GX is present, fill in the location of the application's 
Edit menu items, enable/disable the appropriate menu items, 
and display the Print dialog. If the user clicks OK, print. */ 

editMenuRec.editMenuID = mEdit; 

editMenuRec.cutItem = iCut; 

editMenuRec.copyItem = iCopy; 

editMenuRec.pasteItem = iPaste; 

editMenuRec.clearItem = iClear; 

editMenuRec.undoItem = iUndo; 

MyAdjustMenusForPrintDialogs(true); 

result = GXJobPrintDialog(whichDocument->documentJob, 

&editMenuRec) ; 

MyAdjustMenusForPrintDialogs (false); 

if (result == gxO0KSelected) 
err = MyGXPrintLoop(whichDocument) ; 

} 
else { 

/* If GX is NOT present, open the printer driver and print 
using the Printing Manager. */ 

PrOpen(); 

if (PrJobDialog(whichDocument->documentPrintHd1) ) 
err = MyQDPrintLoop(whichDocument) ; 

PrClose(); 

} 


return err; 


We’ve made a few other changes behind the scenes; see the complete code on the 
CD for details. The code for repaginating a document has been changed slightly to 
use the dimensions of QuickDraw GX page formats, if available, rather than those 
in the rPage field of the old print record. We also made minor changes to our 
MylInsertPage and MyDisposePage routines in order to manage gxFormats on a 
page-by-page basis. In the Simple Sample GX application, we store nil in our 
MyDocumentRec.pageFormat array whenever a new page is created. Because nil is 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


37 


38 develop Issue 19 September 1994 


an invalid gxFormat reference, we can use it to tell the application to use our gxJob 
object’s default format for a given page. If the value stored is non-nil, we can safely 
assume that it’s a valid reference to a custom page format. Finally, in our 
MyDisposePage routine, we now call GXDisposeFormat if the page being removed 
uses a custom page format. 


Since we store gxFormat references on a page-by-page basis, shouldn’t we also 
dispose of page formats in the MyDisposeDocument routine? Well, we certainly can 
do that, but there’s really no need. When we dispose of the document’s gxJob, 
QuickDraw GX automatically disposes of all of the job’s page formats. The flip side 
of this is that you must not attempt to use any gxFormat references once the format’s 
corresponding job is disposed of. 


Continuing with this line of thought, what happens to a document’s page formats 
when the document is saved to disk and later reopened? As it turns out, flattening a 
gxJob causes all of the job’s page formats to be flattened also. This means that the 
code we wrote earlier to flatten and store a document’s gxJob in a resource will also 
store page formats. When we later unflatten the job, the page formats will be 
unflattened as well. 


STORING THE PAGE-TO-FORMAT CORRESPONDENCES 

It’s easy to fall into the trap of thinking that you don’t need to do anything more to 
support saving and loading of by-page formats — but don’t. There are still two more 
issues to consider. 


The first issue is straightforward enough: we haven’t stored our page-to-format 
correspondences, so the next time we open the document we’ll have no idea which 
format goes with which page. The second issue requires a bit more explaining. 


When QuickDraw GX creates a page format, it returns a format reference that’s 
based partially on the reference of the job with which the format is associated. Since 
reference IDs for gxJob objects differ depending on conditions when the job is 
created, a job reference that’s valid when a document is saved is unlikely to be correct 
when the document is later reopened. Similarly, the format references we have when 
a document is saved are unlikely to be correct when the document’s job is later 
unflattened. 


It should now be clear that we can’t simply store our page format reference [Ds with 
a document. To provide the page-to-format information we’ll need when we open the 
document, we have to find another method. Fortunately, there’s a very easy way to do 
this via the Collection Manager. 


As mentioned earlier, every gxFormat has a collection associated with it. QuickDraw 
GX creates these collections automatically when a gxFormat is created. When a 
format is flattened, its collection is also flattened. Specifically, any collection items 
that have the collectionPersistenceBit attribute set are included in the flattened data 
stream. This attribute is set by default when a new collection item is added, so you 
need to change it only if you don’t want a collection item to be included in the 
flattened data. 


‘To store page-to-format mapping, we’ll create a custom collection item. We’ll store 
this collection item in the default format’s format collection. The collection item 
consists of an array of long words — one for each page in the document. In this 
array, we store the index of the format to use for each page, in order. Since format 
indices are preserved during flattening (unlike format references), we'll be able to 
reconstruct the page-to-format relationships when we reopen the document. The 


MySaveFormatRefs routine (Listing 8) saves the format indices. This routine is called 
from our MySavePrintInfo routine, just before we flatten the document’s gxJob (and 


in turn its page formats and format collections). 


Listing 8. MySaveFormatRefs 


#define kMyFormatInfoType 'FLST' 
#define kMyFormatInfoTagID 1000 
OSErr MySaveFormatRefs(MyDocumentPtr whichDocument) 


{ 


OSErr err = noErr; 
Handle theFormatIdxList; 
Collection fmtCollection; 
gxFormat defaultFmt; 


if (whichDocument->numPages > 0) { 
/* Get the job's default format's collection. */ 


defaultFmt = GXGetJobFormat(whichDocument->documentJob, 1); 
fmtCollection = GxXGetFormatCollection(defaultFmt) ; 


/* Create a list of page-to-format correspondences for the 
current document. If there are no errors, add the item to 
the job's default format's collection for later retrieval. */ 


err = MyCreateFormatIndexList(whichDocument, &theFormatIdxList); 


if (err == noErr) { 
HLock(theFormatIdxList) ; 


err = AddCollectionItem(fmtCollection, kMyFormatInfoType, 
kMyFormatInfoTagID, GetHandleSize(theFormatIdxList), 


*theFormatIdxList) ; 
DisposeHandle(theFormatIdxList) ; 


} 


return err; 


Our saved documents now contain information for associating pages with page 
formats. The MyAdjustFormats routine (Listing 9) extracts this information from the 
default format’s format collection when we load the saved document. In effect, the 
code finds new format reference IDs for each format we flattened and stores those 
IDs with the pages that use them. In this way, we completely avoid relying on the old 
(and invalid) format references. 


TRANSLATING AND PRINTING QUICKDRAW COMMANDS 


At long last, we’re ready to look at the code that translates our QuickDraw 


commands to QuickDraw GX shapes and prints them. 


To do the translation, we’ll use the QuickDraw GX translator routines. We specify 
how we would like the translation to be performed by passing one of the 

gx IranslationOptions to the translator routines. (The routines and translation 
options are listed in Inside Macintosh: QuickDraw GX Environment and Utilities.) 
Normally, the default translation options are all you need, and those are what we use 


in Simple Sample GX. 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


39 


40 develop (Issue 19 September 1994 


Listing 9. MyAdjustFormats 


OSErr MyAdjustFormats(MyDocumentPtr whichDocument) 


{ 
OSErr err = noErr; 
Handle theFormatIdxList = nil; 
gxFormat theFormat, defaultFmt; 
long pg, numPages, fmtIdx, *idxList, idx, listSize, attribs; 
Collection fmtCollection; 
defaultFmt = GXGetJobFormat(whichDocument->documentJob, 1); 
fmtCollection = GXGetFormatCollection(defaultFmt) ; 
err = GetCollectionItemInfo(fmtCollection, kMyFormatInfoType, 
kMyFormatInfoTagID, &idx, &listSize, &attribs); 
if (err == noErr) 
theFormatIdxList = NewHandle(listSize); 
if (theFormatIdxList != nil) { 
HLock(theFormatIdxList) ; 
err = GetCollectionItem(fmtCollection, kMyFormatInfoType, 
kMyFormatInfoTagID, dontWantSize, *theFormatIdxList) ; 
numPages = listSize / sizeof(long); 
idxList = (long *) *theFormatIdxList; 
for (pg = 0; (err == noErr) && (pg < numPages); pgtt) { 
fmtIdx = idxList[pg]; 
if (fmtIdx != (long) nil) { 
theFormat = GXGetJobFormat (whichDocument->documentJob, 
fmtIdx); 
err = GXGetJobError(whichDocument->documentJob) ; 
} 
else 
theFormat = nil; 
if (err == noErr) 
whichDocument->pageFormat[pg] = theFormat; 
} 
DisposeHandle(theFormatIdxList) ; 
} 
return err; 
} 


The translation routines come in two varieties — those that take a single QuickDraw 
PicHandle and convert it to a QuickDraw GX picture shape, and those that let you 
execute QuickDraw commands and create equivalent QuickDraw GX shapes as you 
do so. Converting a PicHandle to a gxPicture shape is straightforward; therefore, 
we’re going to take the second approach. Most applications don’t print by simply 
making a QuickDraw DrawPicture call, so understanding how to convert individual 
QuickDraw commands to QuickDraw GX shapes “on the fly” is probably more 
useful. If your needs are different, you can use the GXConvertPICT ToShape routine 
to convert a PicHandle into a gxPicture shape. 


The routines we’ll use are GXInstallQD Translator and GXRemoveQD Translator. 
GXInstallQD Translator tells QuickDraw GX to begin translating QuickDraw 
commands into QuickDraw GX shapes, and GXRemoveQD Translator tells 
QuickDraw GX that we’ve completed drawing. These routines are used in 
conjunction with a third routine, called a gxSpoolProc, which you create. Your 


gxSpoolProc routine will have the following format and will be called whenever 
QuickDraw GX completes a new shape during the translation: 


OSErr MyShapeSpoolProc(gxShape currentShape, long refCon); 


The currentShape parameter contains the QuickDraw GX shape that the translator 
just created, and the refCon parameter is a programmer-defined value that you pass 
to GXInstallQD Translator. You needn’t use the refCon parameter (you can pass nil), 
but as we’ll see in a moment, the refCon can be very handy. 


The code in Listing 10 shows the basic QuickDraw GX print loop, with support 
added to translate QuickDraw commands into QuickDraw GX shapes. 


The code in Listing 10 contains nrequire, require, nrequire_action, and 
require_action macros, which are discussed in the article “Living in an Exceptional 


World” in develop Issue 11. These macros, which don’t require QuickDraw GX 
themselves, are now included in the QuickDraw GX interface file GXExceptions.h.° 


Listing 10. MyGXPrintLoop 


OSErr MyGXPrintLoop(MyDocumentPtr whichDocument) 


{ 
OSErr err; 
long firstPage, lastPage, numPages, pg; 
short oldPage; 
gxViewPort printViewPort; 
Point patStretch = {1,1}; 
gxFormat pageFormat; 
Rect everywhereRect; 
gxShape pageShape; 


MySpoolDataRec spoolData; 


oldPage = whichDocument->curPage; 
/* Determine which pages the user selected to print, and print 
only those pages that are actually in the document. */ 
GXGetJobPageRange(whichDocument->documentJob, &firstPage, &lastPage); 
if (lastPage > whichDocument->numPages ) 
lastPage = whichDocument->numPages; 
/* Calculate the number of pages to print and begin printing. */ 
numPages = lastPage - firstPage + 1; 
err = GXGetJobError(whichDocument->documentJob) ; 
nrequire(err, PageRangeError); 
GXStartJob(whichDocument->documentJob, whichDocument->documentTitle, 
numPages); 
err = GXGetJobError(whichDocument->documentJob) ; 
nrequire(err, StartJobFailed) ; 
/* Create a new view port for printing and set our translator 
rects to "wide open" so that they include all data we're 
drawing. For each page we print, call GXStartPage, draw, 
and call GxXFinishPage. */ 
SetRect(&everywhereRect, 0, 0, 32767, 32767); 
printViewPort = GXNewViewPort(gxScreenViewDevices) ; 


(continued on next page} 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 41 


42 develop Issue 19 September 1994 


Listing 10. MyGXPrintLoop (continued) 


for (pg = firstPage; (err == noErr) && (pg <= lastPage); pgtt) 
{ 

/* Get the page's format and start printing the page. */ 

pageFormat = whichDocument->pageFormat[pg - 1]; 

if (pageFormat == nil) 
pageFormat = GXGetJobFormat(whichDocument->documentJob, 1); 

GXStartPage(whichDocument->documentJob, pg, pageFormat, 1, 

&printViewPort) ; 

err = GXGetJobError (whichDocument->documentJob) ; 

/* If there were no errors, set up the translator, draw 
the QuickDraw data for current page, and remove the 
translator. */ 

nrequire(err, StartPageFailed); 

spoolData.printViewPort = printViewPort; 

GXGetFormatDimensions(pageFormat, &spoolData.pageArea, nil); 

GXInstallQDTranslator (whichDocument->documentWindow, 

gxDefaultOptionsTranslation, &everywhereRect, 
&everywhereRect, patStretch, MyPrintAShape, &spoolData); 
whichDocument->curPage = pg; 

SetPort (whichDocument->documentWindow) ; 

MyDrawContents (whichDocument->documentWindow) ; 

GXRemoveQDTranslator (whichDocument->documentWindow, nil); 

GXFinishPage (whichDocument->documentJob) ; 

} 
StartPageFailed: 
GXFinishJob(whichDocument->documentJob) ; 
err = GXGetJobError (whichDocument->documentJob) ; 
GXDisposeViewPort (printViewPort) ; 
whichDocument->curPage = oldPage; 
StartJobFailed: 
PageRangeError: 
return err; 


Several important things are going on in the code in Listing 10. You may recognize 
the basic QuickDraw GX print loop, which consists of everything in MyGXPrintLoop 
except the view port and translator routines. The first thing we do in the print loop is 
create a gxViewPort object, because GXStartPage needs to know which view ports 
we'll be drawing to. Only shapes drawn in the specified view ports will be printed. 
For our purposes, one view port will suffice, so that’s all we create. 


There are two basic print loop methods for QuickDraw GX: the first uses GXPrintPage 
to print a single gxPicture shape; the second method uses the GXStartPage and 
GXFinishPage routines. In the second method, the application specifies a list of view 
ports, and any QuickDraw GX drawing that occurs in any of these view ports is 
instead redirected to a print file. Since our application draws several shapes on a page, 
it makes sense to use the GXStartPage and GXFinishPage approach. If we had only 
one shape to print (for instance, if we had used GXConvertPICT ToShape), or if our 
gxSpoolProc collected all the converted shapes into one gxPicture shape, using 
GXPrintPage would make more sense. 


Notice that the custom page formats that we’ve added to our application are 
supported, and they require only a couple of lines of code. Recall that in this 
application we’ve decided to use nil to represent the default job format. If the current 
page’s format reference ID is not nil, we pass GXStartPage the reference; otherwise, 
we pass the gxJob object’s default format. This default format is always positioned as 
the first format in a gxJob, so we can obtain it as follows: 


pageFormat = GXGetJobFormat(whichDocument->documentJob, 1); 


Before we can issue our QuickDraw drawing commands, we must call 
GXInstallQD Translator. Because all QuickDraw drawing calls are ignored by the 
QuickDraw GX printing routines, we need to translate all QuickDraw commands 
to QuickDraw GX shapes for printing. In the GXInstallQD Translator call, we 
specify that we want to use the default translation options, that we don’t want to 
stretch patterns, and that our shape-handling routine is called MyPrintAShape. 
Finally, remember the refCon parameter we discussed earlier? Well, here’s where it 
comes into play. In the refCon, we pass a pointer to a MySpoolDataRec, which is 


defined as 
typedef struct MySpoolDataRec { 
gxRectangle pageArea; /* Page rectangle. */ 
gxViewPort printViewPort; /* View port we're printing in. */ 


} MySpoolDataRec, *MySpoolDataPtr; 


‘The MyPrintAShape routine is passed each QuickDraw GX shape that is created as 
the result of the QuickDraw translation. We can print each shape because a pointer 
to our MySpoolDataRec is passed in the refCon parameter of MyPrintAShape. We 
print a shape by attaching the MySpoolDataRec.printViewPort to the current shape, 
and then drawing the shape. We use the page rectangle in the MySpoolDataRec to 
determine whether a translated shape will appear on the printed page. If the shape 
isn’t on the page, it doesn’t make sense to waste time and disk space spooling it. 
Listing 11 shows how cleanly this all fits together. 


Listing 11. MyPrintAShape 


OSErr MyPrintAShape(gxShape currentShape, long refCon) 
{ 

MySpoolDataPtr spoolData; 

gxShapeType theShapeType; 


spoolData = (MySpoolDataPtr) refCon; 
theShapeType = GXGetShapeType(currentShape) ; 


/* Don't waste time spooling the shape if it's being drawn off 
the page. */ 
if ((theShapeType == gxEmptyType) || (theShapeType == gxFullType) | 
(theShapeType == gxPictureType) || 
GXTouchesBoundsShape(&spoolData->pageArea, currentShape)) { 
GXSetShapeViewPorts(currentShape, 1, &spoolData->printViewPort) ; 
GXDrawShape(currentShape) ; 


} 
return (OSErr) GXGetGraphicsError (nil); 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 43 


Back in our print loop, we simply draw our page’s QuickDraw representation 
between the GXInstallQD Translator and GXRemoveQD Translator calls. 
QuickDraw commands are translated to QuickDraw GX shapes and printed in one 
fell swoop. 


PRINT ONE COPY 

A soon-to-be-familiar sign of the QuickDraw GX application will be the Print One 
Copy command in the application’s File menu. The option to print one copy of a 
document without any dialogs is a big convenience to the user, and it requires only a 
few minutes of coding to support. 


In addition to changing the necessary menu setup and handling routines, we need to 
add a routine to support Print One Copy (Listing 12). In this routine, we temporarily 
reset three of the printing options that the user may have previously changed. First, 
we set up the print job so that it prints only one copy. The number of copies last 
printed is stored in the gxJob object, and we want to make sure that if the user 
previously printed multiple copies of a document, only one copy comes out of the 
printer when Print One Copy is chosen. Second, we indicate that we want to print all 
pages of the document, rather than the last page range used. Finally, the output 
should come out of the printer. If the job was last printed to a file, we'll need to change 
the job object’s “Print to disk” setting. Once again, we call upon the Collection 
Manager, although this time we access the job collection. 


Listing 12. MyPrintOneCopy 


OSErr MyPrintOneCopy(MyDocumentPtr whichDocument ) 


{ 

OSErr err; 

Collection jobCollection; 

gxCopiesInfo copiesInfo; 

gxFileDestinationInfo destInfo; 

gxPageRangeInfo pageRangelInfo; 

Ptr oldCopiesInfo = nil, oldPageRangeInfo = nil, 
oldDestInfo = nil; 

long oldCopiesSize, oldPageRangeInfoSize, 
oldDestInfoSize; 


/* Get the job collection and set it up to print one copy. */ 

jobCollection = GXGetJobCollection(whichDocument->documentJob) ; 

/* Set number of copies to 1. */ 

copiesInfo.copies = 1; 

err = MyReplaceCollectionItem(&copiesInfo, sizeof(gxCopiesInfo), 
gxCopiesTag, gxPrintingTagID, jobCollection, &oldCopiesInfo, 
&o0ldCopiesSize) ; 

nrequire(err, ReplaceCopies error); 

/* Set page range to "all". */ 

pageRangeInfo.simpleRange.optionChosen = gxDefaultPageRange; 

pageRangeInfo.minFromPage = 1; 

pageRangeInfo.simpleRange.fromPage = 1; 

pageRangeInfo.maxToPage = whichDocument->numPages; 

pageRangeInfo.simpleRange.toPage = whichDocument->numPages; 

pageRangeInfo.simpleRange.printAll = true; 


(continued on next page} 


44 develop Issue 19 September 1994 


Listing 12. MyPrintOneCopy (continued) 


err = MyReplaceCollectionItem(&pageRangeInfo, 
sizeof (gxPageRangeInfo), gxPageRangeTag, gxPrintingTagID, 
jobCollection, &oldPageRangeInfo, &0ldPageRangeInfoSize) ; 
nrequire(err, ReplacePageRange error); 
/* Set destination to "printer". */ 
destInfo.toFile = false; 
err = MyReplaceCollectionItem(&destInfo, 


sizeof(gxFileDestinationInfo), gxFileDestinationTag, 
gxPrintingTagID, jobCollection, &o0ldDestIinfo, 
&o0ldDestInfoSize) ; 
nrequire(err, ReplaceDestination_ error); 
/* Print one copy of our document. */ 
err = MyPrintDocument (whichDocument) ; 
/* Restore original number of copies, page range, and output 
destination in case anybody uses that info. */ 
ReplaceDestination error: 
MyReplaceCollectionItem(oldDestInfo, oldDestInfoSize, 
gxFileDestinationTag, gxPrintingTagID, jobCollection, nil, nil); 
ReplacePageRange error: 
MyReplaceCollectionItem(oldPageRangeInfo, oldPageRangeInfoSize, 
gxPageRangeTag, gxPrintingTagID, jobCollection, nil, nil); 


ReplaceCopies error: 


MyReplaceCollectionItem(oldCopiesInfo, oldCopiesSize, gxCopiesTag, 
gxPrintingTagID, jobCollection, nil, nil); 
/* Dispose of pointers that MyReplaceCollectionItem created. */ 
if (oldCopiesInfo) 
DisposePtr(oldCopiesInfo) ; 
if (oldPageRangeInfo) 
DisposePtr(oldPageRangeInfo) ; 
if (oldDestInfo) 
DisposePtr(oldDestInfo); 
return err; 


The MyReplaceCollectionItem routine, which I created for the Simple Sample GX 
application, has the format 


OSErr MyReplaceCollectionItem(void *newData, long collectionItemSize, 
OSType collectionType, long collectionID, Collection 
whichCollection, Ptr *oldData, long *oldDataSize); 


This routine replaces a collection item and returns a copy of its old data. It takes a 
pointer to the collection data we want to store, and the size, type, ID, and collection 
to store it in. In the last two parameters, you pass a reference to a pointer in which to 
store the existing data and a reference to a long word in which to store its size. If the 
oldData pointer is nil, the existing data is not returned; otherwise, a new pointer is 
created in the oldData parameter, and the data is returned there. 


MyReplaceCollectionItem allows you to replace a collection item, execute some 


code, and then restore the collection item. ‘That’s exactly what we do in the 
MyPrintOneCopy routine. 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 


45 


46 develop (ssue 19 September 1994 


DRAG-AND-DROP PRINTING 

The final thing that a QuickDraw GX-aware application should support is the new 
attribute of the 'pdoc’ Apple event, enabling users to print documents by dragging 
their icons to desktop printers. You need to make only a few changes to your current 
‘pdoc’ Apple event handler, as you can see from Listing 13. With these changes to the 
Apple event handler, our conversion of the QuickDraw sample application to one 
that’s QuickDraw GX aware is complete. 


For information on the 'pdoc' Apple event, see Inside Macintosh: 
Interapplication Communication, Chapter 4.° 


Listing 13. 'pdoc' Apple event handler, modified for QuickDraw GX 


pascal OSErr MyHandlePDOC(AppleEvent *theAppleEvent, AppleEvent *reply, 
long myRefCon) 


{ 
OSErr err; 
AEDescList docList, dtpList; 
FSSpec myFSS, dtpFSS; 
long itemsInList, i; 
AEKeyword theKeyword; 
DescType typeCode; 
Boolean draggedToDTP = false; 
Size actualSize; 


MyDocumentPtr newDocument; 


/* See if the document was dragged to a desktop printer. */ 
err = AEGetAttributeDesc(theAppleEvent, keyOptionalKeywordAttr, 
typeAEList, &dtpList); 

if (err == noErr) draggedToDTP = true; 

/* If we dragged to a desktop printer, get the name of that 
printer and then throw away the description list for it. */ 

if (draggedToDTP) { 
err = AEGetNthPtr(&dtpList, 1, typeFSS, &theKeyword, &typeCode, 

(Ptr) &dtpFSS, sizeof(FSSpec), &actualSize); 

AEDisposeDesc(&dtpList) ; 

} 

/* Get our document list. */ 

err = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList, 

&docList) ; 

nrequire(err, AEError); 

/* Make sure we've accounted for all of the parameters passed, 
and count the number of documents passed in. */ 

err = MyCheckAEParams(theAppleEvent) ; 

nrequire(err, AEError); 

err = AECountItems(&docList, &itemsInList); 

nrequire(err, AEError); 

/* For each entry in the doc list, load it, print it, and close it. */ 

for (i = 1; i <= itemsInList; itt) { 
err = AEGetNthPtr(&docList, i, typeFSS, &theKeyword, &typeCode, 

(Ptr) &myFSS, sizeof(FSSpec), &actualSize); 

nrequire(err, AEEntryError); 


(continued on next page} 


Listing 13. 'pdoc' Apple event handler, modified for QuickDraw GX (continued) 


/* Load the document. */ 
err = MyCreateDocument(kDefaultTitle, &newDocument); 
nrequire(err, CreateDocFailed); 
err = MyFSLoadDocument(newDocument, &myFSS); 
nrequire(err, LoadDocFailed) ; 
/* If we dragged to a desktop printer, select that as the 
output printer for this job, and print one copy. */ 
if (draggedToDTP) { 
GXSelectJobOutputPrinter (newDocument->documentJob, dtpFSS.name); 
err = MyPrintOneCopy(newDocument) ; 
} 
else /* "Print" chosen from Finder. Show dialog and print. */ 
err = MyPrintDocument (newDocument) ; 
/* Close the document once it's printed. */ 
LoadDocFailed: 
MyDisposeDocument (newDocument) ; 
} 
/* When we're all done, throw away the document list and exit. */ 
CreateDocFailed: 
AEEntryError: 
AEDisposeDesc(&docList) ; 
AEError: 
if (gQuitAfterPrinting) 
gQuitting = true; 
return err; 


WHERE TO TAKE IT FROM HERE 


Now that you know how to add QuickDraw GX printing to a QuickDraw 
application, go do it! Think of all the features you'll instantly support by being 
QuickDraw GX aware. The time hit to gain this level of compatibility is minimal for 
most applications, and well worth it. 


Still not convinced? Take the sample applications and print with both versions under 
QuickDraw GX. Even this simple program shows that if your applications aren’t 
QuickDraw GX aware, you (and your users) are really missing out. 


RELATED READING 


e Inside Macintosh: QuickDraw GX Printing and Inside Macintosh: QuickDraw GX 
Environment and Utilities (Addison-Wesley, 1994). 


e “Getting Started With QuickDraw GX” by Pete (“Luke”) Alexander, develop 
Issue 15. 


¢ “Developing QuickDraw GX Printing Extensions” by Sam Weiss, develop Issue 15. 


Thanks to our technical reviewers Pete (“Luke”) 
Alexander, Hugo Ayala, Tom Dowdy, and Ken 
Hittleman.° 


ADDING QUICKDRAW GX PRINTING TO QUICKDRAW APPLICATIONS 47 


Making the Most of QuickDraw GX Bitmaps 


DAVID SUROVELL 


48 develop Issue 19 September 1994 


Besides letting you do a lot of cool things with geometric shapes and 
typography, QuickDraw GX has useful tools for manipulating bitmaps. 
For example, bitmap shapes (the QuickDraw GX counterpart to 
pixMaps) can be skewed, rotated, and scaled, and transforms allow 
these operations to be performed repeatedly without data loss. Bitmap 
shapes can share image data, can be used to clip other shapes, and can 
reside on disk instead of in memory. This article tells how you can use 
QuickDraw GX to improve the way you handle bitmapped graphics. 


New users of QuickDraw GX will probably start by going through Inside Macintosh: 
QuickDraw GX Objects or the article “Getting Started With QuickDraw GX” in 
develop Issue 15. If you’re mainly a QuickDraw programmer, however, you may have 
a lot of questions about how QuickDraw GX applies specifically to bitmaps — 
probably the most commonly used graphic objects. As it turns out, it can do most 
anything QuickDraw can do, and quite a few useful and exotic new things besides. 


If you have at least a nodding familiarity with QuickDraw GX, this article will give 
useful tips on how to apply your knowledge to bitmap shapes. If you’re a QuickDraw 
GX neophyte, this article will confuse you from time to time, but you may learn 
enough to decide to make the leap to QuickDraw GX. 


CREATING BITMAP SHAPES 


It takes about the same information to create a bitmap shape in QuickDraw GX as 

it does to make a pixMap in QuickDraw. The biggest difference is that while 
QuickDraw insists that you calculate the size of the image buffer and allocate it 
explicitly, QuickDraw GX can optionally allocate it for you when the shape is created. 
This is illustrated in the code in Listing 1, which creates an indexed bitmap shape. 


For indexed pixelSize values (1, 2, 4, or 8), you set the gxBitmap’s space field to 
gxIndexedSpace and its set field to a color set (the QuickDraw GX equivalent of a 
QuickDraw color table) with an appropriate number of entries. Direct pixelSize 
values (16 or 32) require that the set field be nil. For example, to make the routine in 
Listing 1 create a 16-bit bitmap shape, you would set the gxBitmap’s space field to 
gxRGB16Space and its set field to nil. 


DAVID SUROVELL Where there was once one, _ desk at Apple, David's passionate avocations 


there now are three: after approximately 1500 include auditioning as a guitarist for bands that 
years of bachelorhood, David recently married fail to play in public, committing brutal fouls in 
Vane) and achieved fatherhood (Elliot lvan). He otherwise friendly soccer matches and basketball 
once wrote a book on QuickDraw, but that was games, and playing paintball with other rush-hour 
long ago. When he’s not sleeping under his commuters. ° 


Listing 1. Creating an indexed bitmap shape 


gxShape CreateIndexedBitmapShape(long horiz, long vert, 
long targetDepth) 


gxBitmap bitShapeInfo; 
gxColorSet targetSet; 
gxShape resultShape; 


if ((horiz <= 0) || (vert <= 0)) 
return nil; 

if (targetDepth > 8) 
return nil; 


// Create a familiar "color" gxColorSet. 
// (The default gxColorSet is a gray ramp.) 
targetSet = GetStandardColorSet(targetDepth) ; 
if (targetSet == nil) 

return nil; 


// Let QDGX calculate the image buffer block size and allocate it. 
bitShapeInfo.image = nil; 

bitShapeInfo.rowBytes = 0; 

bitShapeInfo.width = horiz; 

bitShapeInfo.height = vert; 

bitShapeInfo.pixelSize = targetDepth; 

bitShapeInfo.space = gxIndexedSpace; 

bitShapeInfo.set = targetSet; 


// Use the default color profile. 
bitShapeInfo.profile = nil; 


resultShape = GXNewBitmap(&bitShapeInfo, nil); 


return resultShape; 


Note that the gxBitmap’s rowBytes is a long, not a short as in QuickDraw. This 
means no more convoluted rowByte hacks, no more magic bits needed for flags, and 
no more unreasonable limits on image width. 


Note also that the gxBitmap contains a profile field, a reference to a gxColorProfile 
(essentially an object with ColorSync data wrapped inside). If this field is nil, 
QuickDraw GX uses its default profile. Color matching occurs only when the target 
view port has the gxEnableMatchPort attribute set — by default, it’s off. 


MANIPULATING BITMAP SHAPES 


Once a bitmap shape is created, you can access and change its characteristics with 
GXGetBitmap and GXSetBitmap. 


GXGetBitmap(targetShape, &bitmapInfo, &origin); 
// Alter the necessary gxBitmap fields here. 


GXSetBitmap(targetShape, &bitmapInfo, &origin); 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 49 


GXSetBitmap is similar to QuickDraw’s UpdateGWorld; it lets you change bitmap 
depth, color specification, and size. To change specific attributes, you may need to 
modify a combination of fields. 


‘To change a bitmap’s width or height, set the width or height field. If QuickDraw GX 
originally allocated the image buffer, you can set rowBytes to 0 and the image field to 
nil, and QuickDraw GX will reallocate the buffer. If you allocated the buffer yourself, 
you'll have to maintain it yourself. 


An image isn’t scaled when you change size this way. If you increase the width or 
height, the new areas contain undefined values; if you decrease them, the image is 


truncated. Bitmap scaling is discussed later in this article. ° 


‘To change a bitmap’s pixel depth, set the pixelSize field to the desired depth. If the 
bitmap needs a new color set (which it will, unless the new depth is greater than 8 
bits), create it and assign it to the set field. An example that changes the depth to 
4-bit is shown in Listing 2. 


‘To change a bitmap’s color characteristics, just change the set, space, and profile 
fields. No changes to pixel data will occur — all pixel values will be interpreted in the 
new color set. ‘To transform pixel values, you’d need to set up a new bitmap shape and 
draw the existing bitmap into it. (The offscreen library routine Copy ToBitmaps is 
ideal for this.) 


PIXEL VALUE REPRE 


SENTATION 


A raster image is, naturally enough, an array of pixel 
values. For indexed color, each pixel value is an index 
into an associated color set. 


For direct color (16 or 32 bits per pixel), a pixel value is 
converted directly into a color value by expanding bit 
fields of the 16- or 32-bit value into three or four 16-bit 
unsigned integer values. 


The expansion of direct pixel values depends on the color 
space of the raster image and the “packing” of the color 
components. QuickDraw supports only RGB and a 


handful of packing schemes, but QuickDraw GX supports 
a whole family of color spaces and packing formats, 
some of which are shown in Figure 1. 


The packing types are defined in the gxColorSpaces 
enum in the header file graphics types.h. You'll also find 
definitions for extended color space specifications, such 
as gxRGB 1 6Space (gxRGBSpace + gxWord5ColorPacking) 
and gxARGB32Space (gxLong8ColorPacking + 
gxRGBASpace + gxAlphaFirstPacking). Only explicitly 
defined permutations are valid — you can’t just make up 
your own. 


1.,2,, 4, or Bbit [Index valve | 
16-bit |x} Red | Green| Blue | gxRGB16Space 
MSB em (SB 
32-bit x Red Green Blue gxRGB32Space 
32-bit Alpha | Red | Green | Blue | gxARGB32Space 
32-bit | x Hue Saturation Value gxHSV32Space 


(Long 10 packing) 


Figure 1. Some QuickDraw GX pixel packing formats 


50 develop Issue 19 September 1994 


Listing 2. Changing the depth of a bitmap shape 
void ChangeDepthToFour(gxShape bitmapShape) 
{ 


gxBitmap imageInfo; 


if ((bitmapShape != nil) && 


(GXGetShapeType(bitmapShape) == gxBitmapType) ) 
{ 
GXGetBitmap(bitmapShape, &imageInfo, nil); 
if (imageInfo.pixelSize != 4) 
{ 
imageInfo.pixelSize = 4; 
imageInfo.space = gxIndexedSpace; 
imageInfo.set = GetStandardColorSet(4); 
GXSetBitmap(bitmapShape, &imageInfo, nil); 
} 
} 


USING DISK-BASED PIXEL IMAGES 


QuickDraw GX provides support for disk-based bitmap shapes. They’re structurally 
the same as regular bitmaps, except that their image data is contained in a file, so 
they’re always drawn from disk. ‘Ten calls to GXDrawShape(diskBitmap) means 
QuickDraw GX reads the entire file from disk ten times. (QuickDraw GX can’t 
assume that you didn’t write into the file between accesses.) The idea is that the file 
system’s disk caches will do the work; if the file wasn’t changed, subsequent reads 
should be cached. 


Make sure the file size is at least as large as the bitmap, or you'll get an 
“unexpected end of file” error.® 


Disk-based bitmaps have limitations. For one thing, certain routines can’t be 
performed on them — GXSetShapePixel, for example. (See Inside Macintosh: 
QuickDraw GX Graphics for the complete list.) You can’t use disk-based bitmap shapes 
as drawing destinations. If you draw into the data you trigger an error. 


So how do you create a disk-based bitmap? As shown in Listing 3, you first set the 
gxBitmap’s image field to gxBitmapFileAliasImageValue. After creating the bitmap 
shape, create a tag of type gxBitmapFileAlias Tag Type containing an alias record that 
references the file containing the target raster data and attach it to the shape. 


ACCESSING IMAGE DATA 

You can manipulate the image data of bitmap shapes directly. If the image data is 
maintained by your application, all you have to do is call GXChangedShape 
afterward. If the image data was allocated by QuickDraw GX, it’s more complicated: 


1. Force the shape to be heap-resident with GXSetShapeAttributes. 
2. Lock the shape with GXLockShape and check for an error. 


3. Call GXGetShapeStructure to obtain a reference to the image 
data. 


4. Read from or write to the image data as desired. 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


51 


Listing 3. Creating a disk-based bitmap 


gxShape CreateDiskBitmap(FSSpec *fsData, gxBitmap *targetBM) 
{ 

gxBitmap localBM; 

gxShape targetShape; 

gxTag targetTag; 


if ((fsData == nil) || (targetBM == nil)) 
return nil; 

targetShape = nil; 

targetTag = CreateBitmapAliasTag(fsData, OL); 

if (targetTag != nil) 

{ 
localBM = *targetBM; 
localBM.image = gxBitmapFileAliasImageValue; 
targetShape = GXNewBitmap(&localBM, nil); 
if (targetShape != nil) 

GXSetShapeTags(targetShape, gxBitmapFileAliasTagType, 
1L, -1L, 1L, &targetTag); 

GXDisposeTag(targetTag) ; 

} 


return targetShape; 


gxTag CreateBitmapAliasTag(FSSpec *bitmapFS, unsigned long fileOffset) 
{ 


struct gxBitmapDataSourceAlias *aliasRecordPtr; 


gxTag targetTag; 

FSSpec targetFS; 

AliasHandle aliasHdl; 

OSErr iErr; 

long aliasSize, aliasRecordSize; 
Boolean wasChanged; 


targetTag = nil; 
aliasHdl = nil; 


aliasRecordPtr = nil; 


// Create an alias and resolve it. 
iErr = NewAlias(nil, bitmapFS, &aliasHdl); 
if (iErr == noErr) 
iErr = ResolveAlias(nil, aliasHdl, &targetFS, &wasChanged); 


// Build up a compact representation for inclusion into a gxTag. 
if (iErr == noErr) 
{ 
aliasSize = GetHandleSize((Handle)aliasHd1) ; 
aliasRecordSize = aliasSize + 2 * sizeof(long); 
aliasRecordPtr = (struct gxBitmapDataSourceAlias*) 
NewPtr(aliasRecordSize) ; 
iErr = MemError(); 


(continued on next page} 


52 develop (Issue 19 September 1994 


Listing 3. Creating a disk-based bitmap (continued) 


// Create the gxTag. 

if (iErr == noErr) 

{ 
// Create a gxBitmapDataSourceAlias with specified fileOffset 
// and appropriate aliasRecordSize and aliasRecord. 
aliasRecordPtr->fileOffset = fileOffset; 
aliasRecordPtr->aliasRecordSize = aliasSize; 
BlockMove(*aliasHdl, &aliasRecordPtr->aliasRecord[0], aliasSize); 
targetTag = GXNewTag(gxBitmapFileAliasTagType, aliasRecordSize, 

aliasRecordPtr) ; 


// Clean up. 

if (aliasHdl != nil) 
DisposeHandle( (Handle) aliasHd1); 

if (aliasRecordPtr != nil) 
DisposePtr((Ptr)aliasRecordPtr) ; 


return targetTag; 


5. If the image data was changed, call GXChangedShape. 
6. Unlock the shape with GXUnlockShape. 
7. Call GXSetShapeAttributes to allow the shape to be cached again. 


GXLockShape loads an image into memory, so it might not succeed if there isn’t 
enough memory. And don’t forget to check a bitmap shape’s space field before 
processing the shape — don’t assume that bitmap images are always in RGB space. 


See Listing 4 for an example of changing a bitmap shape’s data directly. 


MEMORY ISSUES 

Raster surfers and Photoshop junkies know that raster images can be memory hogs; 
it’s easy to run out of application heap when you allocate them. So what happens 
when QuickDraw GX runs out of memory? It doesn’t. Well, almost never. Here are 
the steps it will go through, in order, to deliver the memory you need: 


1. Flush out-of-date caches. 

2. Flush up-to-date caches. 

3. If allowed, grow the current gxHeap. 

4. Unload shapes and other objects to disk. 

5. Give up, and return an error. 
Most QuickDraw developers resort to some sort of GrowZoneProc to handle a 
tight application heap. QuickDraw GX provides a tiered response to abnormal 
occurrences. Items 1 through 4 above return notices (in the debugging version of 


QuickDraw GX); item 5 returns an error. All you have to do is implement a routine 
to handle the notices and errors. 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


53 


Listing 4. Directly changing an indexed bitmap shape 


void InvertBitmapShape(gxShape sourceBits) 


{ 
gxBitmap sourceInfo, *sourceInfoRef; 
gxShapeAttribute curAttributes; 
unsigned char *sourcePtr, *rowPtr; 
long sourceRowSize, structLen, i, j; 
Boolean isQDGXImage; 


// Make sure that this is an indexed bitmap shape. 


if (sourceBits == nil) 
return; 

if (GXGetShapeType(sourceBits) != gxBitmapType) 
return; 


GXGetBitmap(sourceBits, &sourceInfo, nil); 
if (sourceInfo.pixelSize > 8) 


return; 
if (sourceInfo.image == gxBitmapFileAliasImageValue) 
return; 
// If the image data was allocated by QuickDraw GX... 
isQDGXImage = (sourceInfo.image == nil); 
if (isQDGXImage) 
{ 
// Load and lock the image data. 
curAttributes = GXGetShapeAttributes(sourceBits); 
if (!(curAttributes & gxDirectShape) ) 
GXSetShapeAttributes(sourceBits, 
curAttributes | gxDirectShape); 
GXLockShape(sourceBits) ; 
if (GXGraphicsError(nil) != 0) 
return; 
// Get a reference to the image data. 
sourceInfoRef = 
(gxBitmap* )GXGetShapeStructure(sourceBits, &structLen) ; 
if ((sourceInfoRef == nil) || (structLen < sizeof(gxBitmap) ) ) 
return; 
sourceInfo = *sourceInfoRef; 
} 
// Invert index values, one row at a time. 
sourcePtr = (unsigned char*) (sourceInfo.image) ; 
for (i = sourceInfo.height; i > 0; i--) 
{ 


rowPtr = sourcePtr; 
sourceRowSize = sourceInfo.rowBytes; 
while (sourceRowSize-- > 0) 
{ 
*rowPtr = ~*rowPtr; 
rowPtrt+t+; 


(continued on next page} 


54 develop (Issue 19 September 1994 


Listing 4. Directly changing an indexed bitmap shape (continued) 


// Skip to the next row. 
sourcePtr = (unsigned char*)sourcePtr + sourceInfo.rowBytes; 


} 


GXChangedShape(sourceBits) ; 

if (isQDGXImage) 

{ 
GXUnlockShape(sourceBits) ; 
GXSetShapeAttributes(sourceBits, curAttributes); 


GEOMETRIC OPERATIONS 


One of the niftiest features of QuickDraw GX is the ability to perform geometric 
operations on bitmap shapes. Most of the operators that apply to geometric shapes 
also apply to bitmaps: rotate, scale, skew, perspective, and clip. In comparison, 
QuickDraw provides only three geometric operators: scale, clip, and mask. 


ALTERING THE TRANSFORM VERSUS THE GEOMETRY 

When you change a bitmap shape’s geometry (that is, its actual pixel data), whether 
by rotating, skewing, applying perspective, or scaling, you normally lose image data 
— it’s often impossible to return the image to its pristine state. 


You can eliminate this data loss by instead applying geometric operators to a shape’s 
transform. A shape can make use of a 3 x 3 matrix to mathematically change its 
appearance when rendered without changing the underlying data. This is especially 
important for bitmaps. Figure 2 shows both possibilities of multiple rotations of a 
bitmap. 


Hl {ll tj ‘iy ‘ip tip a> ce Geometry rotation 
Hl hy I Mf Up Zi 3 Gy Transform rotation 


Figure 2. Successive rotations of a bitmap 


Rotation, translation (change in origin), skew, perspective, and scale operations can 
all be performed on transforms directly, by GXRotate Transform, GXSkew Iransform, 
and so forth, or indirectly, using the gxMap TransformShape attribute. 


When a shape’s gxMapTransformShape attribute is set, geometric operations 
automatically apply to its transform rather than its geometry. Bitmap and picture 
shapes default to having this attribute set; other shapes begin with it off. This 
means that if you convert a polygon shape (for example) to a bitmap shape, the 
gxMap TransformShape attribute won’t automatically be set. 


When a QuickDraw GX routine modifies a bitmap shape’s geometry, a clip shape is 
often attached to define the geometric extent of the modified bitmap. More often 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


55 


56 develop (lssue 19 September 1994 


than not, the bitmap’s image buffer is expanded, as shown in Figure 3. Rotating a 
bitmap’s geometry can increase its memory requirements by over 40%. 


New bounds 
Original 

bounds 
rE 


Rotate 45° Clip shape 


Original bitmap New bitmap 
Figure 3. Effect of GXRotateShape on bitmap geometry 


ROTATION 

There aren’t many QuickDraw programmers who haven’t wished for a simple way to 
rotate bitmaps. GXRotateShape takes parameters for the target shape, degrees 
clockwise to rotate, and center point of rotation, as shown in Listing 5. 


Listing 5. Rotating a bitmap shape 


void RotateBitmap(gxShape targetShape, Fixed theta) 
{ 

gxBitmap targetBM; 

gxPoint origin, shCenter; 


// Determine the bitmap shape's current center point. 
GXGetBitmap(targetShape, &targetBM, &origin); 
shCenter.x = ff(targetBM.width) / 2 + origin.x; 
shCenter.y = ff(targetBM.height) / 2 + origin.y; 


// Rotate it around its center point. 
GXRotateShape(targetShape, theta, shCenter.x, shCenter.y); 


SKEWING AND PERSPECTIVE 

Skewing and perspective are just as much fun as rotation, and even more useful as 
general-purpose graphic effects. The code in Listing 6 illustrates a simple type of 
perspective; Figure 4 shows the results of this perspective mapping. 


SCALING 

You can expand or shrink bitmap shapes, like other shape types, with GXScaleShape. 
QuickDraw pixMaps are scaled by setting the destination rectangle passed to 
CopyBits, whereas GXScaleShape uses a scaling factor. To convert your QuickDraw 
bitmap scaling code into the equivalent QuickDraw GX code, you have to calculate 
this scaling factor. Listing 7 shows how. 


You can flip a bitmap horizontally or vertically by using negative scaling values. ° 


Listing 6. Applying perspective to a bitmap shape 


void TrapezoidalWarp(void) 
{ 
gxShape bitsShape, warpShape; 
long trapezoidData[] = 
{ 
1L, 4L, 
f££(130), ££(100), f£(170), ££(100), 
f££(200), ££(200), f£(100), ££(200) 
hi 


bitsShape = CreateBasicBitmapShape(); 

warpShape = GXNewShapeVector(gxPolygonType, trapezoidData) ; 

if (warpShape != nil) 

{ 
ShapeSetPolyMap(bitsShape, warpShape); 
GXDisposeShape(warpShape) ; 

} 

GXDrawShape(bitsShape) ; 


void ShapeSetPolyMap(gxShape targetShape, gxShape mappingShape) 


{ 
gxRectangle boundsRect; 


gxPolygon *mapPoly, *targetPoly; 
gxMapping theMapping; 

gxShape targetBounds; 

long ignored; 


if (targetShape == nil) 
return; 
if ((mappingShape == nil) 
| | (GXGetShapeType(mappingShape) != gxPolygonType) ) 
return; 


// Determine the dimensions of the target shape. 
GXGetShapeBounds(targetShape, 0L, &boundsRect) ; 
targetBounds = GXNewRectangle(&boundsRect) ; 
if (targetBounds == nil) 

return; 


// Scale the mapping shape to the dimensions of the target shape. 
GXSetShapeBounds(mappingShape, &boundsRect) ; 
GXSetShapeType(targetBounds, gxPolygonType) ; 


// Load & lock both shapes so that their structures can be accessed. 

GXSetShapeAttributes (mappingShape, 
GXGetShapeAttributes(mappingShape) | gxDirectShape); 

GXLockShape(mappingShape) ; 

GXSetShapeAttributes(targetBounds, 
GXGetShapeAttributes(targetBounds) | gxDirectShape); 

GXLockShape(targetBounds ) ; 


(continued on next page} 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 57 


Listing 6. Applying perspective to a bitmap shape (continued) 


// NOTE: Structure is actually of type gxPolygon. 
mapPoly = (gxPolygon*)GXGetShapeStructure(mappingShape, &ignored); 
targetPoly = (gxPolygon*)GxGetShapeStructure(targetBounds, &ignored); 


if ((mapPoly != nil) && (targetPoly != nil)) 

{ 
// Skip past the gxPolygons contour count to the first contour. 
mapPoly = (gxPolygon*)((Ptr)mapPoly + sizeof(long)); 
targetPoly = (gxPolygon*)((Ptr)targetPoly + sizeof(long)); 


// Calculate the desired shape mapping. 
// PolyToPolyMap() is in "mapping library.c." 
PolyToPolyMap(targetPoly, mapPoly, &theMapping); 


// Release both shapes from bondage. 

GxXUnlockShape(mappingShape) ; 

GXSetShapeAttributes (mappingShape, 
GXGetShapeAttributes(mappingShape) & ~gxDirectShape) ; 

GXUnlockShape(targetBounds ) ; 

GXSetShapeAttributes(targetBounds, 
GXGetShapeAttributes(targetBounds) & ~gxDirectShape) ; 


// Set the target shape's mapping as desired. 
GXSetShapeMapping(targetShape, &theMapping); 


GXDisposeShape(targetBounds ) ; 


Result Result 


Before perspective After perspective 


Figure 4. Applying perspective to a bitmap shape 


CLIPPING AND MASKING 

QuickDraw GX can do some neat tricks with clipping. These tricks work with bitmap 
shapes, too. For example, to create a gradient-filled polygon, you can make a 
rectangular bitmap shape with a gradient and then set the polygon shape as the 
bitmap’s clip shape. (For another example, see Graphical ‘Truffles in this issue.) 


58 develop (Issue 19 September 1994 


Listing 7. Calculating a scaling factor 


void BitmapShapeScaleQDStyle(gxShape targetShape, Rect *qdSourceR, 
Rect *qdDestR) 


gxPoint centerPt; 
fixed scaleFactorH, scaleFactorv; 


scaleFactorH = FixRatio(qdSourceR.right - qdSourceR.left, 
qdDestR.right - qdDestR.left); 

scaleFactorV = FixRatio(qdSourceR.bottom - qdSourceR.top, 
qdDestR.bottom - qdDestR.top); 

centerPt.x = ff((qdDestR.right + qdDestR.left) / 2); 

ff£((qdDestR.bottom + qdDestR.top) / 2); 

GXScaleShape(targetShape, scaleFactorH, scaleFactorV, centerPt.x, 


centerPt.y 


centerPt.y); 
GXMoveShapeTo(targetShape, ff(qdDestR.left), ff(qdDestR.top) ); 
GXDrawShape(targetShape) ; 


You can use 1-bit bitmap shapes as clip shapes, too. The effect is just like that of 
CopyMask; pixels in the source shape are drawn only where the clipping bitmap pixel 
value is nonzero. (On this issue’s CD, you'll also find example code that does image 
processing similar to CopyDeepMask using the new transfer modes.) 


Clipping occurs in geometry space, before transform mapping, so a bitmap’s 
clip shape should be based on its bounds rectangle, not its rendered location.® 


‘To convert geometric shapes into masking bitmap shapes, you can call the 
GXSetShapeType routine to convert the shape to a 1-bit mask bitmap. 


With GXCheckBitmapColor, you can generate a masking bitmap from an existing 
bitmap shape. If you pass GXCheckBitmapColor a color set, it puts 0 in the result 
bitmap for source pixel values that are in the color set. If you pass it a color profile, it 
puts 0 in the result bitmap for source pixel values that are within the color profile’s 
gamut. The result bitmap can be useful for color correction. 


QUICKDRAW GX TRICKS FOR QUICKDRAW DOGS 


QuickDraw GX has ways to do almost anything you can do with QuickDraw. All you 
need to know is how their environments and feature sets compare, and you'll 
understand how to convert from one to the other. 


THE VIEW PORT LIST VERSUS THE GRAPHICS PORT 

Most of the time you won’t have to concern yourself with view ports at rendering 
time, because there’s no sense of the “current port” as there is in QuickDraw. Here’s 
the recommended method for drawing an existing shape into a new view port: 


1. Copy the shape’s transform and install the desired destination view 
port into the copy. 


2. Call GXDrawShape. 
3. Restore the original transform. 


4. Dispose of the copied transform. 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


59 


60 develop (ssue 19 September 1994 


Examples of preserving view port lists can be found in the library routine 
Copy ToBitmaps and in the DrawShapeOffscreen example later in this article 
(Listing 9). 


BITMAPS AND TRANSFER MODES 

QuickDraw GX has a lot of transfer modes. This is a good thing, really. Not all 
transfer modes are functionally equivalent to those in QuickDraw, but the transferMode 
library is fairly complete. Many of the capabilities of QuickDraw search procedures 
can be implemented using transfer modes. (The first page of Inside Macintosh: 
QuickDraw GX Graphics has color pictures of the new transfer modes in action.) 


The transfer mode is contained in a shape’s ink. Since transfer modes are applied on a 
per-component basis, you can easily get some groovy effects. For example, you can 
add the hue of one image to the brightness of another. Usually, though, you'll want 
all components to use the same mode. The transferMode library routine 
SetCommontTransfer will do this for you. 


There are some differences between QuickDraw GX transfer modes and those found 
in QuickDraw: 


¢ Dithering is a view port feature, not a transfer mode. Halftoning is 
also available on a per-gxViewPort basis. These two features are 
mutually exclusive; you can’t dither and halftone at the same time. 


¢ ‘Transparency is not a single mode. It’s a whole family of modes 
based on alpha component values. 


¢ All QuickDraw GX transfer modes occur in color space, while 
some QuickDraw transfer modes are bitwise. 


ONSCREEN BITMAPS 

QuickDraw GX maintains a view device list that mirrors the QuickDraw GDevice 
list. (Utility routines are provided for getting one if you have the other.) The 
Window Manager is patched in a couple of places so that a window’s view port 
transforms and image memory are maintained when it enters and leaves GDevice real 
estate. 


Drawing a bitmap onscreen obeys the screen GDevice’s index entry protections — 
QuickDraw GX doesn’t use indexes reserved by the Palette Manager for other 
applications. If you want to draw an image that uses animated palette entries, you'll 
need to clone references to the destination viewDevice color set and profile, and then 
insert those references into the bitmap shape before drawing. Example code that does 
this is on this issue’s CD. 


COPYBITS IN QUICKDRAW GX 

Let’s see what it takes to make GXDrawShape do what CopyBits does. CopyBits has 
several explicit parameters: the source, destination, clipping region, and transfer 
mode. In QuickDraw GX, the source is the bitmap shape. The destination is defined 
by the shape’s view port list. The clipping region is any shape that you attach to the 
bitmap shape with GXSetShapeClip. As mentioned before, the transfer mode is 
contained in the shape’s ink. 


So, to do a CopyBits-style blit in QuickDraw GX: 


1. Set up the shape’s view port list. 


2. Determine the transfer mode (usually just “copy,” but it’s your 
choice). 


3. Adjust the shape clip. Don’t change the device clip or view port 
clip. 


4. Adjust the transform if you want to reposition, scale, skew, rotate, 
or apply perspective to the shape. 


5. Call GXDrawShape. 
6. Clean up as needed. 


QuickDraw GX doesn’t implement all of the color capabilities of CopyBits. 
There's no colorizing and no color interpolation for indexed values beyond the end of 


a bitmap’s color set.® 


DRAWING OFFSCREEN WITH QUICKDRAW GX 

Successive QuickDraw implementations have presented newer and better ways to 
draw into a offscreen image buffer. The QuickDraw GX offscreen library contains 
routines to help maintain the data structures necessary to implement the equivalent 


of a GWorld. 


The example in Listing 8 uses the CreateIndexedBitmapShape routine from Listing 1 
and the library routine CreateOffscreen to create a fully functional offscreen bitmap. 


Listing 8. Creating an offscreen bitmap 


OSErr MakeIndexedOffscreen(offscreen *targetOffWorld, long horiz, 
long vert, long targetDepth) 


gxShape bitsShape; 


if (!CheckArguments(...)) 
return paramErr; 
bitsShape = CreateIndexedBitmapShape(horiz, vert, targetDepth); 
if (bitsShape == nil) 
return paramErr; 
CreateOffscreen(targetOffWorld, bitsShape) ; 
return noErr; 


You might think drawing into a QuickDraw GX offscreen bitmap would be difficult, 
but it’s not. To draw a shape into the offscreen bitmap, set its view port list to the 
offscreen bitmap’s view port and call GXDrawShape (see Listing 9). 


BITMAP SHAPES VERSUS PIXMAPS 

Sometimes, converting existing QuickDraw code to QuickDraw GX is impractical. If 
your application needs to use the same data in both offscreen pixMaps and bitmap 
shapes, it can, provided that the bitmap shape is packed the same as the pixMap — 
that is, of identical width, height, pixel depth, and color space. 


‘To use bitmap shape data in a QuickDraw pixMap, build the pixMap with the 
baseAddr the same as the gxBitmap.image. (Make sure that the bitmap shape is 
locked down.) To use pixMap data in QuickDraw GX, create a gxBitmap with the 
image field set to the base address of the source pixMap. 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


61 


62 develop (Issue 19 September 1994 


Listing 9. Drawing into an offscreen bitmap 


void DrawShapeOffscreen(offscreen *offGXWorld, gxShape targetShape) 
{ 


gxTransform newXform, savedXform; 


if ((offGXWorld == nil) || (targetShape == nil)) 
return; 

if (offGxXWorld->port == nil) 
return; 


savedXform = GXGetShapeTransform(targetShape) ; 

newXform = GXCopyToTransform(nil, savedXform); 
GXSetTransformViewPorts(newXform, 1L, &(offGXWorld->port) ); 
GXSetShapeTransform(targetShape, newXform); 


GXDrawShape(targetShape) ; 


GxXSetShapeTransform(targetShape, savedXform) ; 
GXDisposeTransform(newXform) ; 


THE QUICKDRAW GX LIBRARIES 


Several libraries are included with the QuickDraw GX Software Developer’s Kit. 
They contain, among other things, routines for offscreen rendering and converting 
image data between QuickDraw and QuickDraw GX. The library code instructs by 
example and is a good starting point for your own library. 


The library code is not completely tested. You should treat it as template 
code, not a final solution.® 


The offscreen library. This library contains support for offscreen bitmaps, copying 
between bitmap shapes, and simple gradient fills. The offscreen image implementation 
is basic but solid (it lacks some of the features found in QuickDraw GWorlds, such as 
automatic longword realignment of images). The utility routine Copy IoBitmaps is 
also useful; it shows a good example of saving a view port list. 


The math library. This library contains a number of useful routines for manipulating 
mappings. The routine Poly loPolyMap is used in the trapezoidal warp example 
(Listing 6). The header file math routine.h contains essential macros for conversion 
between fixed-point, floating-point, and integral values. 


The ramp library. Get your gradient fills here. Pleasing to the eye, easy on the 
code. A gradient-filled bitmap can be rotated and clipped, and voila! Gradient-filled 
shapes. 


The qd and oval libraries. The qd library has facilities for conversion of bitmap 
and color data between QuickDraw and QuickDraw GX formats. The oval library 
has real ovals, not those phony squished QuickDraw things. 


The transferMode library. This library facilitates access to a shape’s transfer mode 
information and contains routines for emulating most of the QuickDraw transfer 
modes. It also contains a bonus — one of my favorite routines. If you’ve ever wanted 


to get the results of a QuickDraw transfer mode on color values without having to 
use CopyBits, TransmogrifyColor is for you. Check it out. 


The storage library. This library implements spooling routines for use with 
GXFlattenShape and GXUnflattenShape, which you'll need for reading and writing 
shapes to and from files. These routines detect errors but don’t report them, so 
they’re only useful as templates. 


The camera library. Perspective is cool, but hard to use unless your math skills are 
well developed. This library provides nifty 3-D techniques. 


AND A FEW MORE THINGS... 


Here I'll point out some caveats and additional interesting features of QuickDraw 
GX, just so you know what to look for (and look out for). 


EXECUTION OVERHEAD 

How fast are QuickDraw GX blits? How slow does an offscreen, 256 x 256, 45°- 
rotated, 32-bit, YXY, gradient-filled bitmap draw into a window on a 4-bit monitor? 
How much for all of these shiny pebbles? It depends. Let’s look at the issues involved. 
QuickDraw GX and QuickDraw have much in common here: 


¢ They’re fastest when there’s no conversion of value or image 
location. 


* Common code paths are optimized inside the API: 8-bit to 8-bit, 
1-bit to 1-bit, 24-bit to 8-bit, no clipping, rectangle clipped. 


¢ Blits involving complex transformations are usually orders of 
magnitude slower. 


Some transformations require more processing. QuickDraw GX does only 
as much work as the transformation matrix mandates. From fastest to slowest, the 
order is: no transformation (or translation only); scaling; skewing or rotation; 
perspective. ® 


The basic performance guidelines are similar to automotive fuel efficiency ratings — 
though we have no hard estimates, mileage is better on a smooth highway (no color 
mapping, skewing, or scaling) than on surface streets. 


A transform mutation can require a 3 x 3 matrix operation for each pixel value when 
rendered. That’s a lot of fixed-point multiplications. If execution speed is critical and 
the mutated version will be used a lot, copy the bitmap shape, mutate the geometry, 

and draw like crazy. Otherwise, mutate the transform and draw as needed. 


SHARED IMAGE BUFFERS 

A bitmap shape’s raster image buffer can be shared by other bitmap shapes. Just make 
the source bitmap shape’s image field the same as that of another bitmap shape. 
GXCopy ToShape uses this sharing of image buffers. If you need a copy of a bitmap 
shape (or a picture that contains bitmap shapes) to have its own image buffer, use 
GXCopyDeep ToShape. 


USING BITMAPS AS PATTERNS 

Bitmap shapes can be used as patterns. Unlike QuickDraw, QuickDraw GX has no 
limitation on area dimension or size of raster data in a pattern. To do simple tiling, 
you can just set the bitmap pattern on the shape. 


MAKING THE MOST OF QUICKDRAW GX BITMAPS 


63 


You can align the pattern to all destination view ports simply by setting the 
gxPortAlignPattern attribute. This forces all shapes drawn with that pattern in a 
given view port to visually line up with each other. Another pattern attribute, 
gxPortMapPattern, keeps a pattern from being affected by a shape’s transform; this 
is useful, for example, when you want a shape rotated and its pattern unrotated. 


BITMAP SHAPE EQUIVALENCE 

You can test QuickDraw GX shapes for equivalence by calling GXEqualShape. 
However, this routine doesn’t account for mapping effects. For example, a bitmap 
gradient from black to white would be considered not equal to a white-to-black 
gradient bitmap whose transform is rotated 180°, even though the two shapes would 
produce identical results when drawn. 


SIMPLIFICATION 

GXSimplifyShape reduces an indexed bitmap to its simplest representation, even 
reducing the pixel depth when possible. For example, if an 8-bit-deep bitmap shape 
contains only 15 colors, GXSimplifyShape will convert it to a 4-bit-deep bitmap. Ifa 
bitmap is all one color, it will be converted into a rectangle shape — it won’t be a 
bitmap shape any more. 


SUBSET EDITING 

QuickDraw GX provides tools for working with area subsets of bitmaps. A piece can 
be copied from a source bitmap via GXGetBitmapParts, edited, and then blasted back 
into the source image with GXSetBitmapParts. 


Individual pixel values can be accessed with the GXGetShapePixel and 
GXSetShapePixel routines. Unlike in QuickDraw, these routines don’t need to 
reference a gxViewDevice to determine the color. 


SO GET GOING 


As you can see, QuickDraw GX does some really cool things with bitmaps. The 
transforms alone make it worthwhile — it’s easy to get addicted to rotating and 
skewing your bitmaps without having to do a lot of work. The new transfer modes 
are great. All the rest is a bonus. In the future, when memory is cheap and every 
machine is fast, you'll see more and more Macintosh systems and applications 
become dependent on QuickDraw GX. 


Thanks to our technical reviewers Pete (“Luke”) 
Alexander, Josh Horwich, and Chris Yerga.® 


64 develop Issue 19 September 1994 


GRAPHICAL 
TRUFFLES 


A Cool 
QuickDraw GX 
Clipping Effect 


Pie 


PETE (“LUKE”) ALEXANDER 


Wading through the vast sea of documentation you get 
with the QuickDraw GX Software Developer’s Kit, you 
may not realize all the cool things this new graphics 
model lets you do. In this column, we'll look at just one 
of those cool things: clipping one shape to the inside of 
another arbitrary shape. As an illustration, we’ll take a 
bitmapped image of a tropical beach (Figure 1) and clip 
it to the word “BAY” (Figure 2). Figure 3 shows the 
result. There’s a sample application and source code on 
this issue’s CD. 


CREATING THE SHAPES 

We'll be collecting the results of our operations into a 
picture shape. We’ll assume that the QuickDraw GX 
environment has already been set up and that we have a 
window ready to draw into. 


For information on setting up the QuickDraw GX environment, 
see “Getting Started With QuickDraw GX" in develop Issue 15. 
The full details are in Inside Macintosh: QuickDraw GX Objects 
and Inside Macintosh: QuickDraw GX Graphics.® 


We start by creating an empty picture to which we can 
add shapes: 


gxShape thePicture; 
thePicture = GXNewShape(gxPictureType) ; 


The bitmap shape we’ll be clipping (Figure 1) is stored 
in our application’s resource fork as a 'pxmp' resource. 
We retrieve it with the GetPixMapShape call from the 
qd library. 


Figure 1. The shape to be clipped 


BAY 


Figure 2. The clip shape 


Figure 3. Result of clipping Figure 1 to Figure 2 


gxShape theBitmap; 
theBitmap = GetPixMapShape(kPixMapID) ; 


To create the clip shape, we start with a text shape 
containing the word BAY. 


PETE (“LUKE”) ALEXANDER Regular readers of Luke’s columns 
will know that he took his sabbatical from Apple this summer. He 
sent us a postcard: “Hey dudes! While you're looking out your 
windows at Silicon Valley smog, I’m looking out my windows at the 
rocky faces of Montana. Have you heard of National Parks? 
They're places of unusual beauty set apart from development. 

Not that kind of development! The kind that causes smog. At 


Yellowstone Park | enjoyed the clear air, | found where the buffalo 
roam, and | learned why they call it Old Faithful. I’m now flying 
somewhere over Montana (don’t worry, I’m steering with my knee), 
admiring the mountains with their lingering snow and enjoying the 
wide open spaces. Oops, mountain ahead and no more room on 
the card.” He signed it, “See you later, Luke” but then crossed out 
the “See you later.” We're worried, really worried. ° 


GRAPHICAL TRUFFLES: A COOL QUICKDRAW GX CLIPPING EFFECT 


65 


gxShape theClip; 
theClip = GXNewText(3, (unsigned char*)"BAY", nil); 


POSITIONING AND SCALING THE TEXT SHAPE 
Before we set our text shape to be the clip of our 
bitmap shape, we need to move it to the top left corner 
of the bitmap shape and then scale it to encompass the 
entire bitmap. 


We'll look at two ways to do this. The first method 
takes us through all the steps in the process, while the 
second is simpler and lets QuickDraw GX do more of 
the work for us. 


The origin of a text shape is on the baseline, but in this 
case, because there are no descenders in the text, we 
just use the bottom left corner of the shape’s bounds. 
We need to offset the text shape by its own height from 
the top of the bitmap shape. So we need to know the 
bounds of both the bitmap shape (to find the 
coordinates of its top left corner) and the text shape (to 
calculate its height): 


gxRectangle bitmapBounds, textBounds; 
GXGetShapeBounds(theBitmap, 0, &bitmapBounds) ; 
GXGetShapeBounds(theClip, 0, &textBounds); 
textHeight = textBounds.bottom - textBounds.top; 
GXMoveShapeTo(theClip, bitmapBounds.left, 
bitmapBounds.top + textHeight); 


Our original text shape, which is the default text size of 
12 points, isn’t big enough to cover the entire bitmap 
shape. If we were to do the clipping at this point, we 
would get just a tiny piece of the corner instead of the 
whole bitmap. To get the whole thing, we have to scale 
the text shape to match the size of the bitmap shape. 


For the text shape to scale linearly, without taking the 
font’s hinting into account, we have to turn off 
QuickDraw GX’s built-in metrics and contour grid- 
fitting capabilities. We do this by setting the shape’s 
text attributes as follows: 


GXSetShapeTextAttributes(theClip, 
gxNoMetricsGridText | gxNoContourGridText) ; 


Although QuickDraw GX lets us clip to any arbitrary 
shape, clipping is actually limited to primitive shapes 
only. That is, the clip shape must be a shape whose 
geometry and fill properties by themselves define the 
shape; primitive shapes don’t use information from a 
style or transform object. This is not a problem, 
though, because we can easily convert a shape to its 
primitive form. The following call does the job: 


GXPrimitiveShape(theClip); 


66 develop Issue 19 September 1994 


The result is a filled path shape (which is a primitive 
shape) representing the outlines of the letters in the 
word BAY. 


Next we need to determine how much to scale the text 
shape to encompass the entire bitmap shape. We do 
this by finding the width and height of both shapes’ 
bounds rectangles and scaling the text shape in each 
direction by the ratio between the two: 


Fixed bitmapWidth, bitmapHeight; 
Fixed textWidth, textHeight; 
Fixed xScale, yScale; 


// Determine the width ratio. 

bitmapWidth = bitmapBounds.right - 
bitmapBounds. left; 

textWidth = textBounds.right - textBounds.left; 

xScale = FixedDivide(bitmapWidth, textWidth) ; 


// Determine the height ratio. 

bitmapHeight = bitmapBounds.bottom - 
bitmapBounds.top; 

textHeight = textBounds.bottom - textBounds.top; 

yScale = FixedDivide(bitmapHeight, textHeight); 


GXScaleShape(theClip, xScale, yScale, 
bitmapBounds.left, bitmapBounds.top) ; 


We’ve now scaled the text shape to encompass the 
entire area of our bitmap shape, and we’re ready to 
use it to clip the bitmap shape. But it turns out that 
there’s a shorter way to do this; we can actually 
accomplish the same thing with only three lines of 
code: 


GXSetShapeTextAttributes(theClip, 
gxNoMetricsGridText | gxNoContourGridText) ; 

GXGetShapeBounds(theBitmap, 0, &textBounds); 

GXSetShapeBounds(theClip, &textBounds) ; 


As in the earlier method, we turn hinting off and get 
the bounds of our bitmap shape. The magic here is 
in the GXSetShapeBounds call: when applied to 
typographic shapes, this call positions and scales the 
text to the new bounds and converts it to a primitive 
shape. That’s it! QuickDraw GX does the hard work 
for us automatically, and we’re ready to set the clip of 
our bitmap shape. 


SETTING THE CLIP SHAPE 

Now that the text shape is positioned and scaled the 
way we want it, we’re ready to set it up as the clip of 
our bitmap shape: 


GXSetShapeClip(theBitmap, theClip); 


This call changes the clip shape contained within the 
transform used by our bitmap shape, replacing it with 
the new clip shape that we’ve created. 


If the same transform were being shared by other shapes, 
QuickDraw GX would make a copy of the transform to associate 
with our bitmap shape, ensuring that the new clip would not affect 
any of the other shapes sharing the same transform.® 


The next step is to add the clipped bitmap shape to our 
picture with GXSetPictureParts. (Note that we could 
also use the library call AddToShape or the core call 
GXSetShapeParts to do the same thing, but I chose to 
be more explicit here.) 


GXSetPictureParts(thePicture, 0, 0, 1, &theBitmap, 
nil, nil, nil); 


The picture now contains a new reference to the 
original bitmap shape, complete with the new clip 
we’ve added to it. Once there’s a reference to it in the 
picture, we don’t need the original reference anymore, 
so we dispose of it: 


GXDisposeShape(theBitmap) ; 


DRAWING THE OUTLINE 

The last order of business is to draw an outline around 
the clipped bitmap. We can accomplish this by setting 
up the drawing characteristics of our scaled text shape 
to draw the outline and then adding it to our picture. 
‘To frame the shape with a closed outline, we set its fill 
characteristic to gxClosedFrameFill. Since we want the 
frame to lie outside the geometry of the shape, we set 
its style attributes to gxOutsideFrameStyle. Finally, we 
set the pen size to 3 to get a satisfyingly fat outline, and 
we set the color of the outline to blue: 


GXSetShapeFill(theClip, gxClosedFrameFill); 
GXSetShapeStyleAttributes(theClip, 
gxOutsideFrameStyle) ; 
GXSetShapePen(theClip, ff(3)); 
SetShapeCommonColor(theClip, blue); 


Now we can add the outlined shape to our picture and 
dispose of it, as before: 


GXSetPictureParts(thePicture, 0, 0, 1, &theClip, 
nil, nil, nil); 
GXDisposeShape(theClip); 


DRAWING THE PICTURE 

Before drawing our picture, we'll move it down a little 
from the top left corner of the window. We could do 
this by retrieving the picture’s transform and shifting it 


down and to the right with GXMove Transform; 
however, since the gxMapTransformShape attribute 
is set for pictures by default, we can just call 
GXMoveShape: 


GXMoveShape(thePicture, £(20), ££(15)); 
Finally, we’re ready to draw the picture: 
GXDrawShape(thePicture) ; 


We're done! The result should look like Figure 3. 


THAT’S ALL, FOLKS 

You’ve had a quick look at one of the many cool things 
you can do with the QuickDraw GX graphics system. 
As you can see from this example, the power and 
flexibility of QuickDraw GX can give your application 
the ability to do things you could only dream about 
until now. 


Thanks to Hugo Ayala and Cary Clark for reviewing this 


column. ° 


Ay 
How’re we doing? 


If you have questions, suggestions, or even gripes 
about develop, please don’t keep them to yourself. 
Let us know what you think. 


Send editorial suggestions or comments to 
AppleLink DEVELOP or to: 

Caroline Rose 

Apple Computer, Inc. 

One Infinite Loop, M/S 303-4DP 

Cupertino, CA 95014 

AppleLink: CROSE 

Internet: crose@applelink.apple.com 

Fax: (408)974-6395 


Send technical questions about develop to: 
Dave Johnson 

Apple Computer, Inc. 

One Infinite Loop, M/S 303-4DP 
Cupertino, CA 95014 

AppleLink: JOHNSON.DK 

Internet: dkj@apple.com 

CompuServe: 75300,715 

Fax: (408)974-6395 


GRAPHICAL TRUFFLES: A COOL QUICKDRAW GX CLIPPING EFFECT 


67 


Pick Your Picker With Color Picker 2.0 


War’ 
et 


SHANNON HOLLAND 


68 develop Issue 19 September 1994 


The limitations of the old Color Picker Package forced many developers 
to write their own color pickers. The flexibility of Color Picker version 
2.0 overcomes the old limitations and provides many new features — 
most notably, use with ColorSync color. Now it’s easy to design color 
pickers to suit your needs. This article describes how to use the new 
Color Picker Manager and take advantage of its customization features 
from within your application. 


Apple designed the Color Picker Package as a way for applications to present a 
standard user interface for color selection. The goal in developing Color Picker 
version 2.0 was to remain compatible with the existing Color Picker Package while 
providing tighter integration of color pickers with the application and allowing 
development of customized color pickers (for example, to support other color spaces 
or specific devices). 


These goals were achieved by adding a Color Picker Manager, turning color pickers 
into components, and separating the color picker components from the Color Picker 
Manager. As components, color pickers are now accessed through the Component 
Manager, which provides a layer between the application and the color picker 
component. In other words, the application calls the Color Picker Manager, which 
then calls the Component Manager, which calls the color picker component. In the 
old Color Picker Package, the application called the color picker directly. 


This separation of the color picker components from the Color Picker Manager 
allows new color picker components to be dynamically added to the system by the 
user or an application. Once a new color picker component has been registered to the 
Component Manager, it’s available for use by the Color Picker Manager. 


The interface to the new Color Picker Manager is divided into high- and low-level 
calls: 


¢ The high-level calls are designed to be used with a minimum of 
fuss, but provide access to nearly the whole feature set available to 
the application through the Color Picker Manager. For 
compatibility with previous versions, the old high-level call, 


SHANNON HOLLAND, once of Apple and Color Picker version 2.0 ships with 

now starting up something elsewhere, had little QuickDraw GX and also with System 7.5. The 
time to write his bio for this article. His only three forthcoming Inside Macintosh: Advanced Color 
activities include working, eating, and sleeping. Imaging will describe Color Picker 2.0 in detail.° 
Once upon a time he had a life in which he 

enjoyed photography, cultural activities, and 

abusing his friends.°® 


GetColor, is still there. A new high-level call, PickColor, replaces 
GetColor and offers a much broader feature set. 


¢ The low-level calls are designed to allow maximum flexibility. 
They let the application determine the type of dialog the color 
picker is placed in, rather than using the modal dialog you get with 
high-level calls. The application can also set the current color and 
maintain explicit control over the event loop. Color pickers that 
are invoked through the low-level calls can exist for the life of an 
application. 


This article discusses how to use these calls and take advantage of the new Color 
Picker Manager. The code examples are provided on this issue’s CD. Color Picker 2.0 
allows multiple color picker components to exist on a system at one time (through the 
Component Manager). Although the interface for these components is public, this 
article doesn’t discuss the creation of color picker components. 


SPECIFYING COLORS 


Unlike the old Color Picker Package, Color Picker 2.0 uses the more complete 
ColorSync definition of a color, which contains both a color and a profile. The 
profile defines the color space of the color (which includes the type of color — 
CMYK, HSL, RGB, and so on). You can also specify a destination profile, which 
describes the color space of the device for which the color is being chosen (for 
example, a color printer that will eventually print the document). Given knowledge of 
the destination profile, color pickers that are ColorSync aware can help the user 
choose a color that’s within the destination device’s gamut. 


ColorSync is described in the forthcoming Inside Macintosh: Advanced Color 
Imaging. See also “Print Hints: Syncing Up With ColorSync” in develop Issue 14.° 


The ColorSync definition for a color, shown below, is used only with the new calls. 
The old call, GetColor, still uses RGBColor for compatibility. These structures are 
compatible with QuickDraw GX. 


typedef struct CMProfile **CMProfileHandle; 


typedef union { 
RGBColor rgb; 
unsigned short reserved[4]; 
} CMColor, *CMColorList; 


typedef struct PMColor { 
CMProfileHandle profile; 
CMColor color; 

} PMColor, *PMColorPtr; 


If you’re specifying an RGB color with no particular profile, you can simply set the 
CMProfileHandle field of PMColor to nil, which uses the system profile. ‘To specify a 
color that uses a profile, you need to provide the profile that describes that color. 


USING THE HIGH-LEVEL CALLS 


The high-level calls are designed to handle the most common uses for the Color 
Picker Manager. The old GetColor call provides access to the new dialog and the 
color picker component, but not to any of the new features that are accessible 
through the Color Picker Manager (such as ColorSync colors). 


PICK YOUR PICKER WITH COLOR PICKER 2.0 


69 


The new PickColor call is designed to replace GetColor. It can be used very simply, 
providing roughly the same feature set as GetColor, or it can be used to take 
advantage of some of the more advanced features of Color Picker 2.0. 


The new dialog for the high-level calls is much the same as the old one. A new 
button, More Choices, reveals a list of all available color pickers (and changes to 
“Fewer Choices”; see Figure 1). Clicking a color picker in the list makes it the current 
color picker for the dialog. Both PickColor and GetColor display this dialog. 


Pick a color, any color: 


90° Original: 


New: 
Apple RGB 
= Hue Angle: [ 187] iq 
Saturation: | 31.56/% A 


270° 


Lightness: | 52.21]7 


Figure 1. Color picker dialog for high-level calls 


The biggest difference between PickColor and GetColor is that PickColor allows the 
application to provide a pointer to an event filter procedure. If the application 
supplies such a procedure, a movable modal dialog will be created rather than the old 
modal dialog. You can do this from within PickColor because you’re now able to pass 
update events to windows within the same application layer as the color picker. 


PickColor also uses the new ColorSync color definition, so you can specify a color in 
any color space along with a destination profile. Likewise, a color can be returned in 
any color space. 


PICKCOLOR PARAMETER BLOCK 

Listing 1 shows the parameter block that you pass through to PickColor. The first 
two fields, theColor and dstProfile, are pretty obvious; they’re simply the input (and 
output) color and the profile for the final output device. If there’s no output device, 
you just set dstProfile to nil. 


The flags field is a little more complicated. (It’s also used in many of the low-level 
calls.) With PickColor, there are three flags you need to worry about: 


¢ CanModifyPalette. If you set this flag, you’re telling the color 
picker component that it’s able to install a palette of its own that 
may modify (but not animate) the current color table. If you don’t 
want the colors in your document to change as you make choices 
in the color picker dialog, don’t set this flag. 


70 develop (ssue 19 September 1994 


Listing 1. PickColor parameter block 


typedef struct ColorPickerInfo { 


PMColor theColor; 
CMProfileHandle dstProfile; 
long flags; 
DialogPlacementSpec placeWhere; 
Point dialogOrigin; 
long pickerType; 
UserEventProc eventProc; 
ColorChangedProc colorProc; 
long colorProcData; 
Str255 prompt; 
MenuItemInfo mInfo; 

Boolean newColorChosen; 


} ColorPickerInfo; 


* CanAnimatePalette. This flag is similar to CanModifyPalette, 
except that it allows the color picker component to modify or 
animate the palette as much as it wants to. 


¢ AppIsColorSyncAware. This informs the Color Picker Manager 
that your application understands ColorSync colors. This means 
that a color may be returned to you in a different space than the 
one you passed in. For example, you could pass an RGB color 
(with no profile) to the Color Picker Manager and receive back a 
CMYK color (with its associated profile). If you don’t set this flag, 
the Color Picker Manager automatically converts any color it 
receives back from the color picker component to RGB space. 


The placeWhere field tells the Color Picker Manager where to position the color 
picker dialog. The choices are kAtSpecifiedOrigin (at the point specified by the 
dialogOrigin field), kDeepestColorScreen (centered on the deepest color screen), and 
kCenterOnMainScreen (centered on the main screen). 


The dialogOrigin field (in conjunction with kAtSpecifiedOrigin) is used when you 
request that the color picker dialog be placed at a specific point. When PickColor 
returns, this field contains the location of the color picker dialog at the time it was 
closed. 


You use the pickerType field to specify the component subtype of the color picker to 
select initially. If you set this field to 0, the default system color picker will be used 
(the last color picker chosen by the user). When PickColor returns, this field contains 
the component subtype of the color picker that was open when the user closed the 
dialog. 


You should set the eventProc field to point to an event filter procedure that will 
handle events meant for your application. If this procedure returns true, the Color 
Picker Manager won’t process the event further. If it returns false, the Color Picker 
Manager will handle the event if it was meant for the color picker. If you set this field 
to nil, a modal dialog will be created (rather than a movable modal dialog). 


The colorProc field can contain a pointer to a procedure that will be called whenever 
the color changes. This allows live updating of colors in application documents as the 


PICK YOUR PICKER WITH COLOR PICKER 2.0 


71 


72 develop Issue 19 September 1994 


user selects them. The colorProcData field contains a long integer that’s passed to the 
color-changed procedure and can be used for any private data. 


The prompt field is a prompt string that the color picker displays to give the user 
some indication as to what the new color is for (for example, a highlight color). 


The mlnfo field tells the Color Picker Manager what the Edit menu ID is and where 
the various menu items are located within it. 


The newColorChosen field is set on return from PickColor. If true, it means that the 
user chose a color and clicked OK; otherwise, the user clicked Cancel. 


IMPLEMENTING PICKCOLOR 

Now let’s look at an example of how all this would be used. Listings 2 and 3 show two 
callbacks — the event filter procedure (MyEventProc) and the color-changed 
procedure (MyColorChangedProc). In the color-changed procedure we assume that 
ColorSync is installed. This is because we’ll be setting the AppIsColorSyncAware flag 
when we call PickColor, so a non-RGB color might come back from the picker and, 
if so, you need to call ColorSync to convert it to RGB. 


Once you have the two callback procedures, you can go ahead and call PickColor (see 
Listing 4). 


USING THE LOW-LEVEL CALLS 


The low-level Color Picker Manager calls are designed to allow tight integration of 
an application and a color picker (a floating palette, for example). Two features make 
this possible: the application can specify the type of dialog to put the color picker in, 
and the application maintains control over the event loop. 


You can create three types of color picker dialogs with the low-level calls: system- 
owned, application-owned, and color picker—-owned. 


° A system-owned dialog is exactly like the dialog created by the high- 
level calls — it has OK, Cancel, and More Choices buttons. 
However, with the low-level calls, you can make the dialog modal, 
movable modal, or modeless. 


¢ An application-owned dialog is actually owned (and supplied) by the 
application. You can use this type of dialog to integrate the color 
picker with other application window features or to extend the 
controls of the color picker. For example, you could add controls 
for altering the style of an object as well as its color. 


° A color picker-owned dialog is created and owned by the color 
picker component itself, which gives that component great 
flexibility because it can determine the size and shape of the color 
picker and the dialog (color pickers in system-owned and 
application-owned dialogs are always the same size). This is useful 
for implementing floating pickers (such as the color wheel in 
Color MacCheese). 


The application interacts with all three types of dialogs in the same way once they’re 
created. The rest of this section describes how to create each type and then moves on 
to discuss how the application interacts with the color pickers, no matter what type of 
dialog you use. In other words, the type of dialog a color picker is in is abstracted 
enough that the application can use roughly the same code to handle all three types. 


Listing 2. Event filter procedure 


WindowPtr myDocWindow; 
pascal Boolean MyEventProc(EventRecord *event) { 
Boolean handled = false; // Assume we don't handle the event. 
switch (event->what) { 
case updateEvt: 
// Check to see if the update is for our window. 
if ((WindowPtr) event->message == myDocWindow) { 
DoTheUpdate(myDocWindow) ; 
handled = true; 


} 


return handled; 


Listing 3. Color-changed procedure 


pascal void MyColorChangedProc(long userData, PMColorPtr newColor) { 


GrafPtr port; 
CWorld cWorld; 
CMColor color; 
CMError cwError; 


GetPort(&port) ; 
SetPort (myDocWindow) ; 


// Now check to see if the color has a profile. If so, we need to 
// convert it to RGB space. 
if (newColor->profile) { 
// Create a color world and convert the color. This color world 
// matches from the color's space to the system space (RGB). 
cwError = CWNewColorWorld(&cWorld, newColor->profile, 0L); 
if (cwError == noErr || cwError == CMProfilesIdentical) { 
// We created the color world. Now match the color using a copy 
// so that we don't munge the original. 
color = newColor->color; 
CWMatchColors(cWorld, &color, 1); 
CWDisposeColorWorld(cWorld) ; 
} 
} else 
color.rgb = newColor->color.rgb; 


// Set the new color and paint the port with it. 
myRGBColor = color.rgb; 
RGBForeColor(&color.rgb) ; 

PaintRect ( &myDocWindow->portRect ) ; 


SetPort(port) ; 


PICK YOUR PICKER WITH COLOR PICKER 2.0 73 


74 


develop (Issue 19 September 1994 


Listing 4. Calling PickColor 


ColorPickerInfo cpInfo; 
PMColor savedColor; 


// Set the input color to be an RGB color in system space. 
cpInfo.theColor.color.rgb = myRGBColor; 
cpInfo.theColor.profile = 0L; 


cpInfo.dstProfile = OL; 
cpInfo.flags = AppIsColorSyncAware | CanModifyPalette | CanAnimatePalette; 


// Center the picker on the deepest color screen. 
cpInfo.placeWhere = kDeepestColorScreen; 


// Use the default picker. 
cpInfo.pickerType = 0L; 


// Install the callbacks. 
cpInfo.eventProc = MyEventProc; 
cpInfo.colorProc = MyColorChangedProc; 
cpInfo.colorProcData = OL; 


strcpy(cpInfo.prompt,"\pChoose a new color"); 


// Tell the Color Picker Manager about the Edit menu. 
cpInfo.mInfo.editMenuID = kMyEditMenuID; 
cpInfo.mInfo.cutItem = kMyCutItem; 
cpInfo.mInfo.copyItem = kMyCopyItem; 
cpInfo.mInfo.pasteItem = kMyPasteItem; 


cpInfo.mInfo.clearItem = kMyClearItem; 
cpInfo.mInfo.undoItem = kMyUndoItem; 


// Save the current color, in case the user cancels. 
savedColor = cpInfo.theColor; 


// And finally, pick that color! 

if (PickColor(&cpInfo) == noErr && cpInfo.newColorChosen) 
// Go use this new color. Remember it can be in any color space. 
DoNewColorStuff (&cpInfo.theColor) ; 

else 
// Canceled or an error; restore old color. 
DoNewColorStuff(&savedColor) ; 


CREATING THE DIALOG 

When creating a system-owned dialog, the application needs to choose whether the 
dialog will be modal, movable modal, or modeless. This is handled by the use of two 
flags: DialogIsModal and DialogIsMoveable. Through obvious combinations of these 
flags, all three dialog types can be created. A nonmovable, modeless dialog (neither 
flag set) is illegal. 


Listing 5 shows the code used to create a modeless system-owned dialog. 


Listing 5. Creating a modeless system-owned dialog 


SystemDialogInfo sInfo; 
OSErr result; 


sInfo.flags = DialogIsMoveable + AppIsColorSyncAware + CanModifyPalette 
+ CanAnimatePalette; 

sInfo.pickerType = OL; 

sInfo.placeWhere = kDeepestColorScreen; 


sInfo.mInfo.editMenuID = kMyEditMenuID; 
sInfo.mInfo.cutItem = kMyCutItem; 
sInfo.mInfo.copyItem = kMyCopyItem; 
sInfo.mInfo.pasteItem = kMyPasteItem; 
sInfo.mInfo.clearItem = kMyClearItem; 
sInfo.mInfo.undoItem = kMyUndoItem; 


result = CreateColorDialog(&sInfo, &myPicker); 


Listing 6 shows how to add a color picker to an application’s own dialog (application- 
owned dialog). 


Listing 6. Creating an application-owned dialog 


DialogPtr myDialog; 
ApplicationDialogInfo aInfo; 
OSErr result; 


// First create the dialog (make sure it's a color dialog so that the 
// color picker can do all the color stuff it needs to do!). 
myDialog = GetNewDialog(kMyDialogID, nil, (WindowPtr)-1); 


// Set up the ApplicationDialogInfo structure. 

aInfo.flags = DialogIsMoveable + AppIsColorSyncAware + CanModifyPalette 
+ CanAnimatePalette; 

aInfo.pickerType = OL; 

aInfo.theDialog = myDialog; 


// Put the color picker's origin at (0,0) in the dialog. 
0; 
0; 


aInfo.pickerOrigin.h 


aInfo.pickerOrigin.v 


// Set the Edit menu information. 
aInfo.mInfo.editMenuID = kMyEditMenuID; 
aInfo.mInfo.cutItem = kMyCutItem; 
aInfo.mInfo.copyItem = kMyCopyItem; 
aInfo.mInfo.pasteItem = kMyPasteItem; 
aInfo.mInfo.clearItem = kMyClearItem; 
aInfo.mInfo.undoItem = kMyUndoItem; 


// Finally, add the color picker to the dialog. 
result = AddPickerToDialog(&aInfo, &myPicker); 


PICK YOUR PICKER WITH COLOR PICKER 2.0 75 


76 


develop Issue 19 September 1994 


Listing 7. Creating a color picker-owned dialog 


PickerDialogInfo plInfo; 
OSErr result; 
piInfo.flags = DialogIsMoveable + AppIsColorSyncAware + CanModifyPalette 
+ CanAnimatePalette; 
pInfo.pickerType = OL; 
pInfo.mInfo.editMenuID = kMyEditMenuID; 
kMyCutItem; 
kMyCopyItem; 
kMyPasteItem; 
kMyClearItem; 
kMyUndoItem; 


pInfo.mInfo.cutItem = 


pInfo.mInfo.copyItem = 
pInfo.mInfo.pasteItem = 
piInfo.mInfo.clearItem = 
pInfo.mInfo.undoItem = 


result = 


CreatePickerDialog(&pInfo, &myPicker) ; 


Listing 7 shows how to create a color picker-owned dialog. As you can see, the code 
to create all three types of dialogs is nearly identical. Likewise the code to manage 
them after creation is very similar. Any explicit differences or requirements will be 
pointed out and explained as they’re encountered. 


SETTING AND GETTING THE CURRENT COLOR 

One of the most obvious requirements for making a color picker useful is that there 
be a way to set and get the current color. This is very simple. Complexities arise only 
if you need to convert colors from the space they’re returned in to a space you can 
understand (such as RGB). The following examples assume you're familiar enough 
with ColorSync to do this (Listing 2 shows how to convert from any space to system 
RGB space). If you don’t want to deal with this, don’t set the AppIsColorSyncAware 
flag and the Color Picker Manager will automatically convert any color it gets back 
from the color picker to RGB. 


The concepts of original color and new color have been carried through from the old 
Color Picker Package to the new Color Picker Manager. Simply put, the original color 
is the color that the user is about to change and the ew color is the color to which the 
user changes it. When setting the color for a color picker, you need to set both 
colors. Suppose, for example, that you’re writing an object-based paint program and 
have created a floating color picker. When the user clicks an object, you want the 
color picker to show the color of that object. You would do this by setting the original 
color and new color for the color picker to the current color of that object. As the 
user changes the color of the object, the original color would remain the same and 
the new color would change. This gives feedback as to what would happen if the user 
were to cancel the color change. The code to do this is very simple: 


void SetPickerToColor(RGBColor *rgb) { 
PMColor aColor; 


aColor.color.rgb = *rgb; 

aColor.profile = OL; 

SetPickerColor(myPicker, kOriginalColor, &aColor); 
SetPickerColor(myPicker, kNewColor, &aColor); 


Whenever the user changes the current color, you need to be able to get the new 
color so that you can update your object accordingly: 


void GetCurrentColor(RGBColor *rgb) { 
PMColor acColor; 


GetPickerColor(myPicker, kNewColor, &aColor); 
*rgb = aColor.color.rgb; 


Some of you might be saying, “But wait, this example is stupid. Isn’t that what the 
color-changed callback is for?” The answer is yes, in the modal case, when the color- 
changed procedure is the only way the application knows that the color changed. In 
the modeless case, as we’ll see below in the section “Giving Events to the Color 
Picker,” the application is informed in other ways when the color changes. So in the 
modeless case, you might want to view the colors that the color-changed procedure 
provides you with as temporary colors and not update your internal data until the 
user has actually chosen a color (or at least stopped dragging on a slider). You should 
then make an explicit call to the Color Picker Manager to get the color, and update 
your internal data. 


SETTING THE DESTINATION PROFILE 

If you’re picking a color for an output device for which you have a ColorSync profile, 
you can give this profile to the color picker component so that it can communicate 
the profile’s information to the user (assuming it knows how). You do this with a 
simple call, SetPickerProfile. There’s also a matching call, GetPickerProfile, to get 
the current profile from the color picker. It’s important to remember that the 
application owns the memory of any profiles it gives or receives from the color picker. 
When you set the destination profile, the color picker component makes a copy of 
the profile handle; when you get the destination profile, you give the color picker 
component a handle into which it copies the profile data. The following code shows 
how to set and get the destination profile. Setting it is optional; the color picker 
assumes that there’s no profile unless you explicitly set one. 


void SetDestinationProfile(CMProfileHandle profile) { 
if (SetPickerProfile(myPicker, profile) != noErr) 
HandleError(); 


void GetDestinationProfile(CMProfileHandle profile) { 
if (GetPickerProfile(myPicker, profile) != noErr) 
HandleError(); 


GIVING EVENTS TO THE COLOR PICKER 

The basic model for giving events to the color picker is similar to DialogSelect. For 
the most part, you give the event to the Color Picker Manager through the 
DoPickerEvent call. It either handles the event or returns it to the application for the 
application to handle. 


There’s one exception to this rule: menus. If you’ve created a modal system dialog, 
the Color Picker Manager can handle the Edit menu events for you (as it does when 
you call PickColor). However, for modeless color pickers there are many menu items 
that the Color Picker Manager has no idea how to handle. If you do send these events 
through to the Color Picker Manager, it will assume all Edit menu selections are 


PICK YOUR PICKER WITH COLOR PICKER 2.0 


77 


78 develop (ssue 19 September 1994 


meant for the color picker and ignore everything else. Therefore, with modeless 
dialogs, the application needs to be sure to handle its own menu events before calling 
DoPickerEvent. 


You'll also need to do some extra work in order for the Color Picker Manager to 
handle the Edit menu correctly. If an Edit menu choice will be for the color picker 
(that is, the color picker dialog is frontmost and the current text item in the dialog 
belongs to the color picker), you need to set up the Edit menu as the Color Picker 
Manager and color picker component want it. To determine how they want the Edit 
menu, call GetPickerEditMenuState. If the user does choose an Edit menu item, the 
application needs to call DoPickerEdit to tell the Color Picker Manager which edit 
operation to perform. There’s more on this later under “Handling the Edit Menu.” 


Each time you call DoPickerEvent and the color picker component or the Color 


Picker Manager handles the event, it returns a constant describing what happened. 
There are several possible results, which are listed in ‘Table 1. 


Table 1. DoPickerEvent return constants 


Constant Meaning 
kDidNothing Nothing happened that’s worth reporting. 
kColorChanged The user changed the color; you may need to call GetPickerColor to 


get the new color. 


kOKHit The user clicked OK; returned only by system- or color picker-owned 
dialogs. 
kCancelHit The user clicked Cancel; returned only by system- or color picker— 


owned dialogs. 


kNewPickerChosen The user chose a new color picker from the More Choices list; returned 
only by system-owned dialogs. 


kApplltemHit The Dialog Manager returned an item intended for one of the 
application's dialog items; returned only by application-owned dialogs. 


Internally, the Color Picker Manager handles the event by calling DialogSelect and 
then processing the event from there. If the color picker is in an application dialog 
and an application item is selected, the Color Picker Manager returns kApplItemHit 
as well as the item number. 


There are a few things to keep in mind regarding the DoPickerEvent return 
constants. How you handle kColorChanged with application dialogs depends on your 
application; for system-owned and color picker-owned dialogs you probably should 
wait until the user clicks OK before treating the color as final. With kOKHit, you 
should save the new color and close the dialog. With kCancelHit, you should restore 
the old color and dispose of the color picker. If kAppltemHit is returned, you need 
to handle the event as you would for the Dialog Manager. You don’t need to care 
about kNewPickerChosen, which happens only with a system-owned dialog. 


If you have a color-changed procedure for the color picker to call, you supply the 
procedure, along with any data it needs to be called with, to DoPickerEvent. 


Listing 8 shows what your event loop might look like. In this code we assume that 
you always want to handle the menu events yourself, as discussed above. 


Listing 8. Sample event loop 


#define IsMenuKey(x) ((x)->what == keyDown && (x)->modifiers & cmdKey) 
Boolean SampleDoEvent(EventRecord *event) { 

Boolean handled = false, isMenuEvent = false; 

EventData  pEvent; 

short inWhere; 

WindowPtr whichWindow; 


// We are assuming that the application always wants to handle menus. 


if (event->what == mouseDown) { 
inWhere = FindWindow(event->where, &whichWindow) ; 
if (inWhere == inMenuBar) 
isMenuEvent = true; 
} 
if (isMenuEvent || IsMenuKey(event)) { 


DoMenu (event) ; 
handled = true; 


// If the event's not handled yet, call the Color Picker Manager to 
// give it a shot. 
if (thandled) { 
pEvent.event = event; 
pEvent.colorProc = MyModelessColorChangedProc; 
pEvent.colorProcData = OL; 
DoPickerEvent(myPicker, &pEvent) ; 
handled = pEvent.handled; 


// If the color picker handled it, we might want to do something 
// with the results. 
if (handled) { 
switch (pEvent.action) { 
case kDidNothing: 
break; 
case kColorChanged: 
UseNewColor(myPicker) ; 
break; 
case kOKHit: 
UseNewColor(myPicker) ; 
DisposeColorPicker(myPicker) ; 
myPicker = nil; 
break; 
case kCancelHit: 
UseOriginalColor(myPicker) ; 
DisposeColorPicker(myPicker) ; 
myPicker = nil; 
break; 
case kNewPickerChosen: 
// You shouldn't care about this. 
break; 


(continued on next page) 


PICK YOUR PICKER WITH COLOR PICKER 2.0 79 


80 develop (Issue 19 September 1994 


Listing 8. Sample event loop (continued) 


case kApplItemHit: 
// Handle the item as you would for the Dialog Manager. 
HandleAppItem(pEvent.itemHit) ; 
break; 


if (!handled) { 
// The event hasn't been handled. Treat it as you would any normal 
// Macintosh event. If you have other dialogs, you need to call 
// DialogSelect. Remember, if the event is a mouseDown, you 
// already called FindWindow! 
} 


return handled; 


FORECAST EVENTS 


When dealing with a color picker, you'll sometimes need to warn it about a user 
action that might affect it. For example, if you have a color picker in an application 
dialog and the user closes that dialog, you might want to see if the color picker is in a 
state that can handle this. If the user had just typed some numbers into the color 
picker that left it in an inconsistent state, it would be nice if the color picker could 
have a chance to complain to the user before it was indiscriminately closed. 


You can do this by using forecast events. These aren’t really events in themselves, but 
are warnings to the color picker. To send forecast events to the color picker component, 
you use the same call as for regular events — DoPickerEvent — except that you set 
the event field to nil and set the forecast field to an appropriate constant. The color 
picker component tells you whether it’s ready for the action to occur by setting the 
handled field of the EventData structure to true if it’s not ready and false if it is. 


For the most part, the only time your application needs to worry about this is when 
the color picker is about to be closed. If the Color Picker Manager has instigated the 
closing (such as when the action field is set to kKOKHit after you called DoPickerEvent), 
you don’t need to worry about telling the color picker component because the Color 
Picker Manager has already done so. However, if the user has just clicked the 
window’s close box (for an application dialog) or has chosen Close from a menu, you 
should send a forecast event to the color picker component. 


The following example shows a function called CheckIfPickerCanClose. If this 
function returns true, the color picker can close; otherwise, it can’t close for some 
reason. It’s safe to assume that the color picker has informed the user of the problem. 


Boolean CheckIfPickerCanClose() { 
EventData pEvent; 


pEvent.event = OL; // Make it a forecast event. 
pEvent.forcast = kDialogAccept; 


DoPickerEvent(myPicker, &pEvent); 


return !pEvent.handled; 


HANDLING THE EDIT MENU 

As mentioned earlier, the Edit menu takes some special work. In addition to standard 
menu processing, if an Edit menu choice will be for the color picker, you need to set 
the state of the Edit menu items according to the color picker specifications and, if an 
Edit menu item is chosen, send the appropriate message to the color picker. This is 
done through two simple calls: GetPickerEditMenuState and DoPickerEdit. 


Once you’ve determined that there has been a mouse-down event in the menu bar or 
a keyboard equivalent has been pressed, you need to determine who owns the Edit 
menu. If the color picker is in a color picker-owned or system-owned dialog and it’s 
frontmost, the color picker obviously owns it. If the color picker is in an application- 
owned dialog and it’s frontmost, ownership of the Edit menu depends on the current 
item. The choice really depends on your application. As a general rule, whoever owns 
the current item owns the Edit menu. If you do call DoPickerEdit while the current 
item belongs to your application, it will implement the standard cut, copy, paste, and 
clear features for you. If your application needs to do more than this, you’ll need to 
handle it yourself. 


In Listing 9 we assume that the owner of the current item owns the Edit menu. The 
item number for the application’s last dialog item is kMyLastItem. If you have a 
system-owned or color picker—owned dialog, this constant should be set to 0. In an 
application-owned dialog the picker’s items will always be added after the 
application’s, so your item numbers remain the same. 


Listing 9. Handling the Edit menu 


Boolean DoMenu(EventRecord *event) { 
long mChoice; 
EditData eData; 
EditOperation eOperation; 


// If picker is in front and current edit item is the picker's, 
// set up the Edit menu the way the picker wants it. 


if (FrontWindow() == myDialog && 
((DialogPeek)myDialog)->editField + 1 > kMyLastItem) { 
MenuState mState; 
MenuHandle theMenu; 


GetPickerEditMenuState(myPicker, &mState); 
theMenu = GetMenu(kMyEditMenuID) ; 
if (mState.cutEnabled) 
EnableItem(theMenu, kMyCutItem) ; 
else 
DisableItem(theMenu, kMyCutItem) ; 
if (mState.copyEnabled) 
EnableItem(theMenu, kMyCopyItem) ; 
else 
DisableItem(theMenu, kMyCopyItem) ; 
if (mState.pasteEnabled) 
EnableItem(theMenu, kMyPasteItem) ; 
else 
DisableItem(theMenu, kMyPasteItem) ; 


(continued on next page} 


PICK YOUR PICKER WITH COLOR PICKER 2.0 81 


Listing 9. Handling the Edit menu (continued) 


if (mState.clearEnabled) 
EnableItem(theMenu, kMyClearItem) ; 
else 
DisableItem(theMenu, kMyClearItem) ; 
if (mState.undoEnabled) { 
SetItem(theMenu, kMyUndoItem, mState.undoString) ; 
EnableItem(theMenu, kMyUndoItem) ; 
} 
else 
DisableItem(theMenu, kMyUndoItem) ; 


// Give the event to the Menu Manager. 
if (event->what == mouseDown) 

mChoice = MenuSelect(event->where) ; 
else 

mChoice = Menukey(event->message); 


// If not the Edit menu, handle normally. 
if (HiWord(mChoice) != kMyEditMenuID) { 
HandleMenuChoice(mChoice) ; 
return true; 


switch (LoWord(mChoice)) { 

case kMyCutItem: 
eOperation = kCut; 
break; 

case kMyCopylItem: 
eOperation = kCopy; 
break; 

case kMyPasteItem: 
eOperation = kPaste; 
break; 

case kMyClearItem: 
eOperation = kClear; 
break; 

case kMyUndoItem: 
eOperation = kUndo; 
break; 

default: 
eOperation 


i] 
! 
an 
~ 


break; 


if (eOperation >= 0) { 
eData.theEdit = eOperation; 
DoPickerEdit(myPicker, &eData); 
// Ignore the results here; assume that the color changed. 
UseNewColor(myPicker) ; 


} 
HiliteMenu(0); 
return true; 


82 develop Issue 19 September 1994 


USING BALLOON HELP 


The Color Picker Manager provides support for Balloon Help. Most applications 
don’t need to do anything special for Balloon Help to work for a color picker in any 
type of dialog. However, for applications in which you need more control over 
Balloon Help, you can call ExtractPickerHelpItem to get the balloon for the color 
picker. It’s up to the application to determine whether the cursor is over a color 
picker’s item or one of its own. The best way to do this is to check to see if it’s over 
one of the application items. If so, put up your own balloon; otherwise, call 
ExtractPickerHelpItem and put up the balloon it returns. ExtractPickerHelpItem will 
ask the color picker for a balloon and search the color picker’s help resource for an 
appropriate balloon. If it can’t find one, it returns the error noHelpForltem. 


The hardest part about using ExtractPickerHelpItem is determining which item the 
cursor is over. Fortunately, there’s a Dialog Manager call, FindDItem, that does the 
dirty work for you. Listing 10 shows how you would use these calls. Everything in 
this example is actually done by the Color Picker Manager internally; the example 
just gives you a general idea of how to use the ExtractPickerHelpItem call. 


Listing 10. Using ExtractPickerHelpltem 


void DoBalloonHelp(void) { 
HelpItemInfo helpInfo; 


short itemNo; 
Point where; 
OSErr err; 


GetMouse(&where) ; 
itemNo = FindDItem(myDialog, where) + 1; 


// Go and get the color picker's help item. 

helpInfo.options = 0; 

helpInfo.tip.v = helpInfo.tip.h = 0; 
SetRect(&helpInfo.altRect, 0, 0, 0, 0); 

helpInfo.theProc = 0; 

helpInfo.variant = 0; 

helpInfo.helpMessage.hmmHelpType = 0; 
helpInfo.helpMessage.u.hmmPictHandle = OL; 

err = ExtractPickerHelpItem(myPicker, itemNo, 0, &helpInfo); 


// Show the balloon if we found one. 
if (err == noErr) { 
// Tf altRect is empty, we need to use the item's rectangle. 
if (EmptyRect(&helpInfo.altRect)) { 
short iType; 
Handle iHandle; 


GetDItem(myDialog, itemNo, &iType, &iHandle, &helpInfo.altRect); 


// Convert the tip to dialog coordinates. 
helpInfo.tip.h += helpInfo.altRect.left; 
helpInfo.tip.v += helpInfo.altRect.top; 


(continued on next page} 


PICK YOUR PICKER WITH COLOR PICKER 2.0 83 


84 develop (Issue 19 September 1994 


Listing 10. Using ExtractPickerHelpltem (continued) 


// Convert the tip and altRect to global coordinates. 
LocalToGlobal(&helpInfo.tip); 

LocalToGlobal((Point *) &helpInfo.altRect.top); 
LocalToGlobal((Point *) &helpInfo.altRect.bottom) ; 


// Finally, put the balloon up. 


HMShowBalloon(&helpInfo.helpMessage, helpInfo.tip, 
&helpInfo.altRect, OL, helpInfo.theProc, helpInfo.variant, 
kHMRegularWindow) ; 


TAKE YOUR PICK 


You should now have a general idea of how to use the new Color Picker Manager. 
Most applications will need only the high-level calls. However, developers who use 
color more thoroughly may want to take advantage of the low-level calls. The 
low-level calls were designed to be very flexible and easy to use. The simple 
implementations shown in this article are trivial; more complicated uses are possible, 
and shouldn’t be much harder to write. 


Having experimented with the new features of Color Picker 2.0, you may still want to 
write your own color picker component — for example, to implement your own 
floating color picker. The new Color Picker Manager makes it easier for you to write 
your own color picker component and allows you to share it among several 
applications (and make it available for general system use as well). 


So take your pick of the color pickers already available through the high-level or low- 
level calls or move beyond this article to create your own custom color picker 
component. Either way, you’re looking at a colorful future with Color Picker 2.0. 


Thanks to our technical reviewers Don Moccia, 
Konstantin Othmer, and David Surovell.*® 


SOMEWHERE IN 
QUICKTIME 


Media Capture 
Using the 
Sequence Grabber 


JOHN WANG AND 
FERNANDO URBINA 


A very important and often overlooked feature of 
QuickTime is the standardization of media capture. 
Since its initial release, QuickTime has defined an API 
for capturing different types of media, including video 
and sound. This API, known as the sequence grabber, 
makes it possible to easily add media capture to any 
application. 


Not only are applications that use the sequence grabber 
able to automatically support any QuickTime- 
compatible media capture hardware, but they also 
perform flawlessly and efficiently regardless of system 
configuration. ‘This is not an easy task considering all 
the variations in hardware features and system 
configurations. In fact, we’re even hesitant to say that 
the sequence grabber “supports video and sound 
capture” because the sequence grabber API also 
insulates the programmer from the actual media type 
being captured. The sequence grabber supports any 
media type, and, with the release of QuickTime 2.0, 
users can automatically capture the new music media 
type in addition to sound and video. 


‘To demonstrate the proper use of the sequence 

grabber, we’ve included on this issue’s CD a simple, but 
complete, sequence grabber application — all in about 
10K of compiled C code! If you’re looking for a general 
all-purpose capture application that’s efficient, reliable, 
and best of all, customizable, look no further. After a 
brief introduction to the sequence grabber, we’ll discuss 
the sample code, and then end with some special 


considerations for media capture on Macintosh AV 
models. 


WHAT IS THE SEQUENCE GRABBER? 

‘The sequence grabber is actually a component of type 
‘barg' (read it backwards). Although the specification 
for the component is completely defined in Chapter 5 
of Inside Macintosh: QuickTime Components, it’s very 
unlikely that you’ll ever want to implement your own 
‘barg’ component. Instead, you’ll be using this 
component specification as the API definition for the 
standard sequence grabber. 


‘The sequence grabber component implements the 
basic functionality of media capture. For handling 
specific media-related functions, the sequence grabber 
calls on various sequence grabber channel components 
(as defined in Chapter 6 of Inside Macintosh: QuickTime 
Components); there’s one such component for each 
media type. Before QuickTime 2.0, the two standard 
channel components available were the video and 
sound sequence grabber channels, enabling the 
sequence grabber to capture video and sound media. 
QuickTime 2.0 includes the new music sequence 
grabber channel, allowing real-time capture of music 
from MIDI instruments. 


Sequence grabber panel components (described in 
Chapter 7 of Inside Macintosh: QuickTime Components) 
manage items in a settings dialog box that allows the 
sequence grabber to obtain configuration information 
from a user. Applications typically don’t use sequence 
grabber panel components directly; instead, the 
sequence grabber automatically uses them for relevant 
sequence grabber component calls. 


USING THE SEQUENCE GRABBER 

Using the sequence grabber is as simple as opening the 
sequence grabber component and calling SGInitialize 
(complete error checking can be found in the sample 
code on the CD): 


theSG = 
OpenDefaultComponent(SeqGrabComponentType, 0); 
SGInitialize(theSG) ; 


JOHN WANG (AppleLink WANG.JY) While writing the 
sequence grabber sample code for this column, John watched the 
movie Top Gun so many times that he can now duplicate each and 
every air combat scene with his favorite flight simulator, FA/18 
Hornet. John once aspired to become a private pilot, but that idea 
was quickly quelled once his significant others found out. As Skate 
so succinctly put it, “Woof woof wooof?” Translation: “Who's 
going to feed me if you kill yourself?”® 


FERNANDO (“NANO”) URBINA (AppleLink NANO) uses his 
Macintosh AV to capture the views of the Rockies from his home 
office in Colorado Springs. He still doesn’t understand how it can 
thunder and snow at the same time, but thinks he’ll be able to 
figure this out once he adjusts to the lack of oxygen. Nano suffers 
severe withdrawal from his favorite coffee shop near the Apple 
campus in Cupertino, but manages to get a fix about once a 
month when he returns there. He worked on the original AV 
models and is now a member of the second-generation AV team. ° 


SOMEWHERE IN QUICKTIME: MEDIA CAPTURE USING THE SEQUENCE GRABBER 


85 


It’s also important to call SGSetGWorld to set the 
window used for displaying any visual data. 


SGSetGWorld((**myWindowInfo) .thesSG, 
(CGrafPtr) myWindow, nil); 


Opening the channel components. Now it’s a 
matter of calling SGNewChannel to open a sequence 
grabber channel component to access a particular 
channel media type. However, rather than hard-coding 
the media types into the sample application, as in the 
call 


SGNewChannel(theSG, VideoMediaType, 
&videoChannel) ; 


it’s better to use the Component Manager to search for 
all the different sequence grabber channel components 
and open a connection to each one. This guarantees 
that the capture application can automatically support 
new media types such as the music media type in 
QuickTime 2.0. 


For example, the following code compiles a list of 
sequence grabber channel components: 


cd.componentType = SeqGrabChannelType; 
cd.componentSubType = 0; 
cd.componentManufacturer = 0; 
cd.componentFlags = 0; 
cd.componentFlagsMask = 0; 

aComponent = 0; 


for (i=0, done=false; i<kMAXCHANNELS && !done; 
itt) { 
aComp = FindNextComponent(aComp, &cd); 
if (aComp != 0) { 
// Get the channel name and type. 
gSGInfo.channelName[i] = NewHandle(4); 
GetComponentInfo(aComp, &theCD, 
gSGInfo.channelName[i], nil, nil); 
gSGInfo.channelType[i] = 
theCD.componentSubType; 
} else 
done = true; 


This list of component types can then be used to open 
a connection to each of the media types with 
SGNewChannel, or SGNewChannelFromComponent 
if the channel component is already open. 


Saving and restoring settings. We want the sample 
application to start up each time with the same channel 
settings and video compression settings as when the 
application was last used. To implement this, we use a 


86 develop Issue 19 September 1994 


preferences file to store these settings. The 
compression settings are restored with two sequence 
grabber calls: 


SGSetVideoCompressorType ( 
(**myWindowInfo).channel[videoChannel], 
gSGInfo.cInfo.compressorType) ; 


SGSetVideoCompressor ( 
(**myWindowInfo).channel[videoChannel], 
gSGInfo.cInfo.depth, nil, 
gSGInfo.cInfo.spatialQuality, 
gSGInfo.cInfo.temporalQuality, 
gSGInfo.cInfo.keyFrameRate) ; 


The channel settings are restored by a simple call to 
SGSetChannelSettings with the settings retrieved from 
the preferences file: 


SGSetChannelSettings(theSG, channel[i], 
channelSettings[i], 0); 


Previewing. We’re almost ready to begin previewing. 
But note that some sequence grabber channel 
components require additional calls before they can be 
used. For instance, spatial channels such as video 
require a call to SGSetChannelBounds to set the 
channel’s display boundary rectangle. So, once the 
channels are created and the previous settings are 
restored as discussed above, we make a call to 
SGSetChannelBounds for the video media to set the 
video capture to encompass the entire window. We also 
call SGSetChannel Usage for all sequence grabber 
channels, which tells the sequence grabber that we 
want to preview and record every channel. 


‘To start previewing, we simply call SGStartPreview. 
However, while we’re previewing, any changes to the 
system must be handled with care. First, we’ll pause the 
preview whenever an event that requires updating of 
the channel information occurs. For example, if the 
capture window is dragged, we’ll pause the video, 

move the window, and then unpause the video. 
Likewise, if we resize the window, we’ll want to pause 
the preview, resize the window, and then unpause the 
preview: 


// Pause the sequence grabber before resizing. 
SGPause((**myWindowInfo).theSG, true); 


// Resize and then update the video channel. 
SizeWindow(theWindow, width, height, false); 
MyUpdateChannels (theWindow) ; 


// OK. We can restart again. 
SGPause((**myWindowInfo).theSG, false); 


Notice the call to MyUpdateChannels. This is a 
routine in the sample application that updates the 
video bounds and channel usage by calling 
SGSetChannelBounds and SGSetChannelUsage. 


The user configuration dialog. Another feature that 
needs to be handled in a capture application is the user 
configuration dialog for each of the different capture 
medias. This is actually one of the simplest things to 
deal with because the sequence grabber component 
handles everything. It even stores the settings 
internally. To retrieve the settings, we can simply call 
SGGetChannelSettings at a later time. In the sample 
application, we get the channel settings before we close 
the connection to the sequence grabber. Then we save 
the settings in the preferences file. 


This is all the code necessary to display and handle the 
user configuration dialog: 


SGSettingsDialog(theSG, theChannel, 0, nil, 0, 
nil, 0); 


Recording. The last important part of the sample code 
is sequence grabber recording. Before recording can 
begin, we need to specify an output file with 
SGSetDataOutput so that the sequence grabber knows 
where to save the captured media data: 


StandardPutFile("\PName of new movie:", 
&reply); 
if (!reply.sfGood) 


"\pMovie", 


return; 
SGSetDataOutput(mySG, &reply.sfFile, 
seqGrabToDisk) ; 


Then we start recording by simply calling 
SGStartRecord(theSG) ; 


We loop and call SGIdle until the mouse button is 
pressed to stop recording. This is the most efficient 
way to record: we don’t want to call WaitNextEvent, 
since that would give other processes time. Instead, we 
want to hog the CPU time until the recording process 
is done. 


while (!Button() && !err) { 
err = SGIdle(theSG); 
} 


We stop recording and start previewing again as 
follows: 


SGStop(mySG) ; 
SGStartPreview((**myWindowInfo) .theSG); 


And, of course, just to be nice, we flush the mouse- 
down events so that no application switching takes 
place after the mouse button is pressed: 


FlushEvents(mDownMask, 0); 


That’s really all there is to the sequence grabber sample 
application. 


SPECIAL CONSIDERATIONS FOR AV MODELS 

As mentioned earlier, one of the key features of the 
sequence grabber is its ability to work with all hardware 
and system configurations. This is not an easy task 
considering all the different types of video capture 
boards. For example, there are boards that are simply 
frame grabbers, and there are those that support on- 
board hardware video compression. ‘Io make every 
configuration work, the sequence grabber has to handle 
every case. Here we'll discuss the unique features of the 
Macintosh AV models and some steps you can take to 
improve their capture rate. 


The video-in circuitry allows the AV models to display 
16-bit color and 8-bit grayscale. And, although the 
hardware can’t display video-in at 24 bits per pixel, you 
can capture video using YUV 4:2:2 compression and 
achieve an effective 24 bits per pixel. To capture in 
YUV, you must use the AV’s video digitizer hardware 
compression feature, which you can do simply by 
selecting “Component Video - YUV” from the list of 
compressors in the Compression panel of the video 
settings dialog. You should also make sure that you 
haven’t checked a “Post Compress Video” or similar 
checkbox in a movie-grabbing application. Selecting 
this checkbox would bypass the hardware compression, 
and the sequence grabber would grab the data in raw 
RGB format. 


The AV circuitry can’t display video when it’s capturing 
the compressed data. The sequence grabber realizes 
that it needs to decompress the data into the capture 
window in order to give the visual feedback that’s 
normally expected. This is fine and dandy, but since 
there’s no hardware decompression in the system, the 
image decompression is completed in software. ‘This 
degrades the capture rate. 


Knowing that the decompression during recording is 
what’s hurting the capture rate, you can easily rectify 
the problem by turning off preview during recording so 
that decompression into the capture window won’t take 
place. To do this, you just call SGSetChannelUsage for 
the video channel with the seqGrabPlayDuringRecord 
flag set to 0. In the sample code, a menu selection 
allows you to turn off video playthrough during 
recording. 


SOMEWHERE IN QUICKTIME: MEDIA CAPTURE USING THE SEQUENCE GRABBER 


87 


The downside of using YUV compression is that 
playback without hardware decompression isn’t very 
smooth because of the high data rate and raw 
processing power needed to decompress each pixel. 
After capturing, you should recompress the movie 
using a compressor such as Cinepak or Video that 
provides a better playback rate. 


GO MAKE A MOVIE 

‘The sequence grabber obviously makes the job of 
media capture simpler. But there are many other 
factors that can play a part. Hard drive transfer rate, 
disk fragmentation, SCSI bandwith, sound settings, and 
AppleTalk activity all play an important part in limiting 
the maximum capture rate. You can also maximize the 
capture rate by rebooting with no AppleTalk 
connections. You should also experiment with the 
different sound sample rates, as these also affect the 
capture rate. 


New additions to the sequence grabber in QuickTime 
2.0 also help. Instead of capturing to a single movie file, 
it’s now possible to specify a different file for each 


Thanks to Peter Hoddie and Don Johnson for reviewing this 


column. ° 


channel. For example, you can record video to a large 
and fast external hard drive and record audio to the 
internal hard drive. ‘This optimization allows for better 
allocation of resources and better efficiency because 
each channel has higher bandwidth. Using the sample 
code, if QuickTime 2.0 is installed, you can select 
recording to separate files. 


There are, of course, other optimizations that can be 
explored. With a bit of creativity and testing, you can 
achieve the optimal capture rates. 


RELATED READING 


e Inside Macintosh: QuickTime Components, 
Chapters 5-7, and Inside Macintosh: More 
Macintosh Toolbox, Chapter 6, “Component 
Manager” (Addison-Wesley, 1993). 


e “Video Digitizing Under QuickTime” by Casey 
King and Gary Woodcock, develop Issue 14. 
About the sequence grabber and video capture. 


development products. 


copy of the APDA Tools Catalog. 


tools for developers 


Your main source for Apple development products 


Get easy access to New Inside Macintosh and hundreds of other programming products, tools, 
technical resources, and information through APDA, Apple’s worldwide source for Apple and third-party 


Ordering is easy, and APDA offers convenient payment and shipping options, including site licensing. 


Call for additional information on APDA or recently announced products, or to request a complimentary 


Call APDA today. 
United States 1-800-282-2732 
Canada 1-800-637-0029 
International (716) 871-6555 


8s develop Issue 19 September 1994 


Implementing Inheritance In Scripts 


aK 
PAUL G. SMITH 


“Programming for Flexibility: The Open Scripting Architecture” in 
develop Issue 18 showed you how to use scripts to increase your 
program’s flexibility. This article builds on that one and explains how to 
implement an inheritance scheme in your application that will enable 
your AppleScript scripts to share handlers and properties and to support 
global variables. Yow'll also learn a way to support inheritance in other 
OSA languages with just a little extra work. 


In Issue 18 of develop, I showed you how to attach scripts to application-domain 
objects and how to delegate the handling of Apple events to those scripts. I left you 
with a challenge: to figure out how to support global variables and to enable scripts to 
share subroutines and handlers. To meet this challenge you need to implement 
inheritance. The AppleScript 1.1 API gives you all the necessary calls to implement 
inheritance in embedded AppleScript scripts, but not all are documented yet in Inside 
Macintosh. This article documents the calls you need and describes an inheritance 
scheme that relies on them. 


In a nutshell, here’s the scheme: 
1. Decide what kind of script inheritance hierarchy to use. 
2. Link your scripts together in an inheritance chain. 


3. Create a shared handlers script to define subroutines and Apple 
event handlers that are shared among all scripts. Make this script 
the parent of all other scripts in your program, in effect putting it 
at the end of the inheritance chain. 


4. Create a global variables script and add this to the start of the 
inheritance chain so that it’s the first script to receive incoming 
messages. Save this script to disk when the program exits and 
reload it when the program restarts, so that variables persist. 


You can use much the same scheme to implement inheritance in other Open 
Scripting Architecture (OSA) languages, but more work is required to link scripts 
together in an inheritance chain, and you must forgo the luxury of sharing global 
variables between scripts. At the end of this article, the section “Inheritance in Other 
OSA Languages” describes the extra work your program must do. 


PAUL G. SMITH (AppleLink COMMSTALK.HQ) services to corporate clients, and watch his cat, 


took time out from his preferred occupation of Mack, dismember his Macintosh’s mouse. He was 
snoozing on a beach to write this article. He also —_— the lead developer of AgentBuilder and wrote 
occasionally takes time out to write software for ScriptWizard, the AppleScript debugger, before 
Full Moon Software Inc., provide consultancy he found his true, totally prone, calling.® 


IMPLEMENTING INHERITANCE IN SCRIPTS 


89 


The sample program SimpliFace2 on this issue’s CD demonstrates the inheritance 
mechanisms discussed here. SimpliFace2 is an extension of SimpliFace, the basic 
interface builder used to illustrate the article in Issue 18. The SimpliFace2 sample 
code has a compile-time flag qUseOSAinheritance, defined in the header file 
SimpliFace2Common.h. If this flag is undefined, the program uses the AppleScript- 
specific inheritance scheme described in the bulk of this article. If the flag is defined, 
SimpliFace2 uses a general-purpose scheme that involves the extra work outlined in 
the section on inheritance in other OSA languages. 


CHOOSE A SCRIPT INHERITANCE HIERARCHY 


The first thing to do is to decide what kind of script inheritance hierarchy to use. You 
can use a runtime containment hierarchy (like that used by HyperCard and FaceSpan™), 
a class hierarchy (like that used by AgentBuilder), or some hybrid of the two, as 
demonstrated by SimpliFace2. Let’s look at each of these hierarchy types in turn. 


FaceSpan (formerly Frontmost) is the interface builder bundled with the 
AppleScript 1.1 Software Development Toolkit. Perhaps the best known OSA “client,” 
FaceSpan was developed by Lee Buck (of “WindowScript” fame) of Software Designs 
Unlimited, Inc. AgentBuilder, from commstalk hq and Full Moon Software Inc., is a 
framework for the creation of communications and information-processing agents that 
uses embedded OSA scripts to customize agent behavior. °® 


Figure 1 shows a runtime containment hierarchy. In this kind of hierarchy, objects 
inherit behavior from their containers at run time. In the object containment 
hierarchy used by HyperCard, for example, the scripts of buttons and fields within 
cards are at the bottom of the hierarchy. Above them are the scripts of the cards that 
contain the buttons and fields, and above each card script is the script of the 
background that contains the card. Above each background script, in turn, is the 
script of the stack that contains all the backgrounds. The handlers in each container’s 
script are shared by the scripts of all the objects it contains. 


Figure 2 shows a class hierarchy. In this kind of hierarchy, objects inherit behavior 
from their parent classes. For instance, the behavior of AgentBuilder objects is 
defined in an “ancestor” object of each class, which is the parent of all instances of 
that class. This permits the standard scripted behavior of object classes to be 
overridden in derived class instances. 


Application Default behavior 
object in Apple event 
handler code 
A 


Window 
object 


“Standard” 
button 
View View Button 1 Button 2 
object object instance instance 


Button Field 
object object 


Figure 1. A runtime containment hierarchy Figure 2. A class hierarchy 


90 develop (Issue 19 September 1994 


Figure 3 shows the hybrid script inheritance hierarchy used in SimpliFace2. In 
SimpliFace2, the scripts of user-interface objects — such as windows, labels, and 
buttons — are organized so that they inherit behavior from the runtime containment 
hierarchy. However, the script of the application object isn’t included in the 
inheritance chain for the script of any user-interface object, and the shared handlers 
script becomes the ultimate parent of all other scripts. I chose to use this hybrid 
hierarchy in order to demonstrate a wider range of techniques, not for any reason 
intrinsic to the program design. 


Shared 
handlers 
Application Window 
object object 
Field 
object 


Figure 3. The hybrid script inheritance hierarchy used in SimpliFace2 


The kind of script inheritance hierarchy to use depends on the nature of the messages 
being handled in your program. Using a class hierarchy is most appropriate if the 
messages are Apple events defined in the program’s 'aete' resource. If the incoming 
messages are primarily user-defined subroutines being handled inside scripts, using a 
runtime containment hierarchy is probably more natural for the scripter. 


Another way to look at this choice is that if you want to enable users to customize 
your program’s capabilities by attaching scripts to application-domain objects, using a 
runtime containment hierarchy isn’t always the best idea. Because different 
application-domain objects handle the same Apple event message in different ways (in 
other words, the semantic meaning of the message differs depending on what object 
it’s directed at), unwanted side effects could result from an object’s handling an Apple 
event message intended for a different level in the containment hierarchy. Using a 
class hierarchy ensures that messages will be dealt with only by objects of the class 
that understands them. 


Once you’ve chosen the type of script inheritance hierarchy most appropriate for 
your program, you can link scripts together in an inheritance chain. 


LINK SCRIPTS IN AN INHERITANCE CHAIN 


Linking scripts together in an AppleScript inheritance chain is as simple as setting 
their parent properties. Before I tell you how to do that, though, let’s review a few 
facts about script objects and inheritance. As mentioned in the Issue 18 article, a 
script context (a script compiled using the AppleScript OSA component) is equivalent 
to a script object in the AppleScript language, so everything I say here about script 
objects applies to script contexts as well. 


ABOUT APPLESCRIPT SCRIPT OBJECTS AND INHERITANCE CHAINS 

Script objects can contain global variables, properties, and handlers for Apple event 
messages and subroutine calls. A script object can have as its parent property an 
object specifier or another script object. Thus, one script object can become the 


IMPLEMENTING INHERITANCE IN SCRIPTS 


91 


92 develop (Issue 19 September 1994 


parent of another, and the child script object can inherit properties and handlers from 
the parent script object. Parent and child script objects are linked together in an 
inheritance chain; this is the path from child to parent to grandparent and so on in an 
inheritance hierarchy, as illustrated in Figure 4. 


For (a lot) more on object specifiers, see “Apple Event Objects and You” by 
Richard Clark in develop Issue 10.° 


Script attached to 
application object 


End of chain 
(grandparent} 


Script attached to 
window object 


(parent) 


Script attached to 
button object 


(child) 


Start of chain 


Figure 4. A script inheritance chain 


An incoming Apple event message is received by the child script object at the start of 
the inheritance chain. If AppleScript can’t resolve a reference to a handler or variable 
name within this script object, it searches through the entire inheritance chain to find 
it. The handler or variable is resolved wherever it’s found in the inheritance chain. 
When a handler continues a message (that is, passes the message to its parent), 
AppleScript starts searching in its parent script object. Messages that target objects 
outside the program’s domain, or that aren’t handled anywhere in the script 
inheritance chain (such as Apple events defined in the program’s 'aete’ resource, 
which are handled in the program code instead), or that are continued out of the 
inheritance chain, are redispatched as Apple events. 


SETTING A SCRIPT’S PARENT PROPERTY 

Now that you understand the dynamics of script inheritance, I’ll show you how to set 
a script’s parent property and thus link it to an inheritance chain. In the AppleScript 
language, you simply say what you'd like the parent set to, as illustrated here: 


script mom 
on getName () 
return "Fenella" 
end getName 
end script 


script toddler 
property parent : mom 

on getName () 
set myMom to continue getName() 
return "Bart, son of " & myMom 

end getName 


end script 


getName() of toddler --> returns "Bart, son of Fenella" 


To set the parent of a script context from a programming language, you can use the 
AppleScript routine OSASetProperty. This general-purpose routine (defined in the 
header file ASDebugging.h, which was added with the AppleScript 1.1 API) accesses 
either a predefined property or a user-defined variable, depending on the AEDesc 
passed to it. To access a predefined property — the parent property — you create a 
descriptor of typeProperty (ot type Type), specifying the property ID as the data. 
The parameters to the call are (1) the scripting component (probably the AppleScript 
component), (2) a mode flag (we use the null mode, indicating no special action 
should be taken; alternatively, we could instruct AppleScript to replace the property 
only if it already exists), (3) the script context ID that’s to be changed, (4) the 
AEDesc, and (5) the value you’re setting the property to, in our case the new parent. 
The OSA routine OSAGetProperty performs the converse function: you can use it to 
inspect the values of properties and variables. 


Here’s a fragment from SimpliFace2 that sets the parent of a script by calling 


OSASetProperty: 
OSAError err = noErr; 
AEDesc nameDesc; 


DescType thePropCode = pASParent; 


err = AECreateDesc(typeProperty, (Ptr)&thePropCode, sizeof(thePropCode), 
&énameDesc) ; 
if (err == noErr) { 
err = OSASetProperty(scriptingComponent, kOSAModeNull, contextID, 
&nameDesc, newParentID); 
AEDisposeDesc(&nameDesc) ; 


} 


The structure of the inheritance chain is static; each parent link needs to be set up 
only once, as long as no scripts are replaced. The only exception to this is that the 
parent property of the global variables script used in SimpliFace2 needs to be set 
every time an incoming Apple event message is handled, as I’ll explain later. 
Whenever a script in the chain is replaced by a new one, the script’s OSAID will 
change and you'll need to set the parent property in the new script and in its children 
again. 


STRIPPING COPIED PARENT SCRIPTS 

By setting the parent properties of scripts and thus linking them in inheritance 
chains, your program limits unnecessary duplication of script objects. Still, when 
AppleScript sets the parent of a script, it stores a copy of the script’s parent (and of 
the parent’s parent, and so on) with the original script. This is the basis of the trick 
that allows SimpliFace to simulate sharing scripts between objects: every script carries 
with it a copy of all the scripts it shares. But this is wasteful — it means that, for 
instance, each button script for a window contains a copy of the window’s script, 
when only one copy is necessary. Because your program is directly controlling script 
inheritance chains, you'll want to block this behavior when it loads and stores scripts. 
You can do it by specifying the KOSAModeDontStoreParent flag when you call 
OSAStore and recreating the inheritance chain when the scripts are reloaded. 


Listing 1 shows the routine used to set the script property of an object in 
SimpliFace2. Note how it’s changed from the routine used in SimpliFace: it now 
strips the copied parent scripts from the incoming script so that SimpliFace2 can 
manage the inheritance chain itself. 


IMPLEMENTING INHERITANCE IN SCRIPTS 


93 


Listing 1. TScriptable Object: :SetProperty 


OSErr TScriptableObject::SetProperty (DescType propertyID, 
const AEDesc *theData) 


OSAError err = errAEEventNotHandled; 


switch (propertyID) { 
case pScript: 
OSAID theValueID = kOSANull1Script; 
if (theData->descriptorType == typeChar 
|| theData->descriptorType == typeIntlText) 
err = OSACompile(gScriptingComponent, theData, 
kOSAModeCompileIntoContext, &theValueID); 
else { // If it's not text, we assume script is compiled. 
err = OSALoad(gScriptingComponent, theData, kOSAModeNull, 
&theValueID); 
// The following new section strips any existing parent script. 
if (err == noErr) { 
AEDesc newData; 
err = OSAStore(gScriptingComponent, theValueID, 
typeOSAGenericStorage, 
kOSAModeDontStoreParent, 
kOSAModeDontStoreParent, &newData); 
if (err == noErr) { 
OSADispose(gScriptingComponent, theValueID); 
theValueID = kOSANul1Script; 
err = (OSErr)OSALoad(gScriptingComponent, &newData, 
kOSAModeNull, &theValueID); 
AEDisposeDesc(&newData) ; 


} 

if (err == noErr) { 
if (fAttachedScript != kOSANul1Script) 

OSADispose(gScriptingComponent, fAttachedScript); 

fAttachedScript = theValueID; 
err = SetCurParent(fParent0bj); 
// This fixes up the references in any object that 
// has the current object as its parent. 
this->FixUpScriptReferences (this) ; 


} 


break; 


} 


return (OSErr)err; 


ATTACH SHARED HANDLERS AND GLOBAL VARIABLES 
SCRIPTS 


Now the plot thickens. You’re going to use the inheritance chain you’ve set up to 
make it possible for your program’s AppleScript scripts to share handlers and 
properties and to support global variables. 


94 develop (Issue 19 September 1994 


Our strategy, as demonstrated in SimpliFace2, is to attach a shared handlers script to 
the application object and a global variables script to the global script administrator 
object (which, as in SimpliFace, is responsible for fetching the script attached to 
objects and preparing it for execution). The shared handlers script, which as a 
convenience for the scripter ’ve made a property of the application object (in 
addition to the application object’s attached script), defines common subroutines for 
all the object scripts known to the program. This script is added to the end of the 
inheritance chain so that it becomes the parent of all other scripts. Globals are 
created in the global variables script, which is always inserted at the start of the 
AppleScript inheritance chain when an Apple event is handled by a script. 


Let me explain why the global variables script is necessary. The AppleScript OSA 
component creates global variables in the script context that received the current 
message, the one at the start of the inheritance chain. If the variables weren’t 
predefined as properties when the parent script was defined, they won’t be visible to 
all scripts. But we’d like variables that are declared in handlers with the Global 
keyword to be available to handlers in all scripts. We achieve this by adding the global 
variables script that we create to the start of the inheritance chain. Thus, the only 
script actually to be dispatched messages is the global variables script, and its parent 
becomes the currently resolved object’s script. Because its parent changes to whatever 
is the appropriate script in the inheritance chain, messages are handled by the correct 
targets. 


HOW IT ALL WORKS 

The AppleScript-only inheritance approach demonstrated in SimpliFace2 works like 
this: Whenever the Apple event prehandler receives an event that might be handled 
in a script, it tries to resolve the object that should handle it. If it can’t resolve an 
object it assumes that the application object’s script should handle the event instead. 
It then attaches the global variables script (the script that receives all incoming 
events) to the start of the inheritance chain that ends with the shared handlers script 
(the parent of all scripts). In between is the script of the object to which the event was 
targeted; if that object is a button or a field in a window, the inheritance chain also 
contains the window’s script. The result is the inheritance chain shown in Figure 5. 


Shared 
handlers script 


Window 
object script 


Button 
object script 


Global 
variables script 


Apple event 
message 


Figure 5. The script inheritance chain used in SimpliFace2 


IMPLEMENTING INHERITANCE IN SCRIPTS 


95 


The Apple event prehandler routine in SimpliFace2 calls the global script 
administrator object to manage the scripts, as shown in this extract from the 
prehandler: 


TScriptableObject* theScriptableObj = NULL; 
TScriptableObject* savedParent = NULL; 


if (err == noErr && theToken) 
err = gScriptAdministrator->GetAttachedScript (theToken->GetTokenObj(), 
theScriptableObj, savedParent); 
if (err == noErr) { // Pass to script for handling. 
if (theScriptable0bj) 
err = ExecuteEventInContext(theEvent, theReply, theScriptableObj) ; 
else 
err = errAEEventNotHandled; 
if (theToken) 
gScriptAdministrator->ReleaseAttachedScript (theToken->GetToken0bj(), 
savedParent); 


The script administrator function GetAttachedScript (Listing 2) is responsible for 
adding the global variables script to the start of the inheritance chain by setting its 
parent to the script of the target object. Here’s how it works: First, it asks the 
scriptable object that’s the target for the Apple event message to deliver its attached 
script. If there’s no attached script, the parent of the global variables script is set to be 
the shared handlers script. If the object does have an attached script, that script 
becomes the parent of the global variables script. 


Listing 2. TScriptAdministrator::GetAttachedScript 
OSAError TScriptAdministrator::GetAttachedScript ( 
TScriptableObject* theObj, 
TScriptableObject* &useObject, 
TScriptableObject* &savedParent) 
{ 
OSAError err = noErr; 
OSAID theObjScript = kOSANu11Script; 
if (the0bj) 
theObjScript = theObj->GetObjScript(); 
if (theObjScript != kOSANullScript) 
err = StartUsing(theObj, savedParent); 
else // If target has no script, new parent is shared handlers. 
err = StartUsing(NULL, savedParent); 
if (err != noErr) 
useObject = NULL; 
else 
useObject = this; // Tf OK, return global variables script. 
return err; 
} 


GetAttachedScript returns the ID of the global variables script so that the prehandler 
can send the Apple event to the inheritance chain that the script now starts. The 


96 develop (Issue 19 September 1994 


global variables script receives the Apple event message in the function 
ExecuteEventInContext, called from the prehandler. The StartUsing function in 
Listing 3, which is inherited by the script administrator from TScriptableObject, 
returns the current parent of the script so that it can be saved for subsequent 
restoration by the script administrator function ReleaseAttachedScript. 


Listing 3. TScriptableObject::StartUsing 


OSAError TScriptableObject::SetCurParent (TScriptableObject* theParent) 
{ 

OSAError err = nokErr; 

OSAID newParentScriptID = GetParentScript(); 


if (fAttachedScript != kOSANullScript) 
err = gScriptAdministrator->SetScriptParent (gScriptingComponent, 
fAttachedScript, newParentScriptID); 
return err; 


OSAError TScriptableObject::StartUsing (TScriptableObject* newParent, 
TScriptableObject* &oldParent) 


oldParent = fParentObj; // Returned in case it must be saved. 
return SetCurParent(newParent) ; 


The parent of the global variables script must be saved and restored every time the 
Apple event prehandler handles an Apple event. This is because Apple events are 
dispatched recursively during the execution of scripts: you should assume that any 
Apple event handler can be interrupting the processing of another Apple event. For 
the same reason, if you need to set the resume/dispatch procedure differently in 
different handlers you must carefully save and restore it each time. The sample code 
in SimpliFace2 contains examples of how you might do this. 


SimpliFace2 also shows how you can make global variables persistent, by saving the 
global variables script in a script file in the Preferences folder when the program exits 
and reloading it when the program starts up again. The code to handle this is in the 
script administrator routines SaveGlobalVariables and LoadGlobalVariables. 


A RUN HANDLER WRINKLE 

Be aware of a wrinkle: If you ever intend to dispatch the Run (‘oapp') Apple event to 
an AppleScript inheritance chain, each script in the chain must contain a continue 
run handler. 


on run 
continue run 
end run 


The reason for this is that when AppleScript compiles a script into a script context it 
collects all the top-level statements in the script (those not contained in any handler) 
into a default run handler, so that if the script is simply executed the top-level 
statements will run. If there are no top-level statements, an empty run handler is 
created. The trouble is, this default handler doesn’t realize you want it to continue 


IMPLEMENTING INHERITANCE IN SCRIPTS 


97 


98 develop Issue 19 September 1994 


the Run message, so the message will be caught and lost in the script, never to be 
seen farther along in the inheritance chain. 


INHERITANCE IN OTHER OSA LANGUAGES 


The inheritance scheme just described is specific to AppleScript scripts because it 
relies on OSASetProperty to set up the inheritance chain. This call and others in the 
AppleScript 1.1 API aren’t part of the required OSA API, so not all scripting 
components support them. If your program is to support inheritance in scripts 
written in other OSA languages, it must take control of the message passing between 
scripts in the chain at script execution time. 


Your application can do this by simulating, entirely under program control, the 
mechanism that AppleScript uses. Messages that aren’t handled in a particular script 
in the chain are passed along to the next script in the chain. If they’re continued out 
of, or not handled in, the chain, the program routes them to its standard Apple event 
handlers. The drawback to this approach is that scripting becomes a little more rigid, 
because handlers are resolved only toward the parent rather than wherever they are in 
the chain. The advantage is that it will work with any OSA language that supports the 
event handling API and the Subroutine Apple event, which is the message protocol 
AppleScript uses to call subroutines. 


When your program’s Apple event prehandler deals with an incoming Apple event 
message by passing it to a script, it’s responsible for manually redispatching messages 
that aren’t handled or that are continued along the inheritance chain. The 
SimpliFace2 routine ExecuteEventInContext (Listing 4) shows how to do this. The 
first and second parameters to this routine are the Apple event and reply; the third 
parameter is the scriptable object that is to handle the message. This routine is first 
called from the prehandler, which passes it the scriptable object that starts the 
inheritance chain (the object to which the message was originally sent). The 
scriptable object has a field that’s a reference to its parent object, which can be read 
using the accessor function GetParentObj. If the current object has a parent, the 
routine sets the OSA resume/dispatch procedure to be a recursive call to 
ExecuteEventInContext, passing the address of the parent object as the reference 
constant. If the current object doesn’t have a parent (if the end of the chain has been 
reached), the routine sets the resume/dispatch procedure to be the program’s 
standard Apple event handler, ignoring the prehandler. 


If the Apple event message isn’t handled in the script of the current object, the routine 
OSADoEvent returns the error errAEEventNotHandled. At this point you must 
manually redispatch the message, mirroring the OSA’s resume/dispatch mechanism: if 
the current object has a parent, you recursively call ExecuteEventInContext, passing 
it the address of the parent object. If you’ve reached the end of the inheritance chain, 
you simply call the program’s standard Apple event handler. 


WHERE YOU’VE BEEN, WHERE YOU’RE GOING 


You’ve learned that implementing inheritance starts with choosing an appropriate 
script inheritance hierarchy. With this hierarchy in mind, you can link scripts in an 
inheritance chain, either by setting their parent properties (if you’re working only 
with embedded AppleScript scripts) or by directly controlling inheritance at script 
execution time (if your program needs to support scripts in other OSA languages). 
Then you’re ready to create a shared handlers script and put it at the end of the 
inheritance chain, making it the parent of all scripts, and (if you’re working only with 
AppleScript scripts) to add a global variables script to the start of the inheritance 
chain where it can receive incoming messages and route them to the correct targets. 


Listing 4. ExecuteEventlnContext 
static pascal OSErr ExecuteEventInContext (AppleEvent *theEvent, 
AppleEvent *theReply, 
TScriptableObject* theScriptable0b}) 
{ 
OSAError err = errAEEventNotHandled; 
OSAID theScriptID = kOSANullScript; 
if (theScriptable0bj) 
theScriptID = theScriptableObj->GetObjScript(); 
else if (gScriptAdministrator) 
theScriptID = gScriptAdministrator->GetSharedScript(); 
if (theScriptID != kOSANullScript) { // Pass to script for handling. 
AEHandlerProcPtr oldResumeProc; 
long oldRefcon; 
TScriptableObject* parentObj = NULL; 
OSAGetResumeDispatchProc(gScriptingComponent, &oldResumeProc, 
&0ldRefcon) ; 
if (theScriptableObj) { 
parentObj = theScriptableObj->GetParent0Obj(); 
err = OSASetResumeDispatchProc(gScriptingComponent, 
(AEHandlerProcPtr) &ExecuteEventInContext, 
(long) parentObj); 
} 
else 
err = OSASetResumeDispatchProc(gScriptingComponent, 
kOSAUSeStandardDispatch, kOSADontUsePhac) ; 
if (err == noErr) { 
err = OSADoEvent(gScriptingComponent, theEvent, theScriptID, 
kOSAModeAlwaysInteract, theReply); 
if (err == errAEEventNotHandled) { // Not handled in script. 
if (theScriptableObj) // Make recursive call. 
err = ExecuteEventInContext(theEvent, theReply, parentObj); 
else // Otherwise, dispatch directly to standard handler. 
err = StdAEvtHandler(theEvent, theReply, 0); 
} 
} 
OSASetResumeDispatchProc(gScriptingComponent, oldResumeProc, 
oldRefcon) ; 
} 
return (OSErr)err; 
} 


You’ve seen these techniques illustrated by the sample program SimpliFace2. You can 
simply drop the classes from SimpliFace2 into your own programs, use them as the 
basis of new programs, or use them as a guide to restructuring existing programs. 
Armed with the information in this article and its predecessor, you should now be 
able to implement an inheritance scheme for scripts in your own software. 


Thanks to our technical reviewers Kevin 
Calhoun, Ron Karr, and Jeroen Schalk, and to Lee 
Buck of Software Designs Unlimited.® 


IMPLEMENTING INHERITANCE IN SCRIPTS 


Macintosh 
Q&A 


1 00 develop Issue 19 September 1994 


Q What operations cause QuickDraw GX to invalidate its shape caches? We want to 


A 


maximize drawing performance in our application, and it would be nice to know ahead 
of time when caches will need to be rebuilt. 


As you might imagine, this topic is complex, but we’ll try to explain the basics 
and give you some ideas about improving performance. 


‘There are several caches associated with a shape, since each object associated 
with the shape has a separate cache. (By the way, the caches are in the 
QuickDraw GX heap and can be seen using GraphicsBug.) Every time you call 
GXDrawShape, QuickDraw GX determines which caches need to be updated, 
updates them, and then draws the shape. So the first thing to know is that if you 
want GXDrawShape to execute as fast as possible (for instance, if you’re 
drawing several shapes and want the time between successive GXDrawShape 
calls to be minimal), you should call GXCacheShape ahead of time to update 
the shape’s caches, minimizing the work GXDrawShape needs to do. 


In general, any time you change information within a shape, you cause 
QuickDraw GX to invalidate at least one of the shape’s caches. Layout shapes 
are exceptions to this rule, however. The cache associated with a layout shape 
will not be invalidated if all of the following conditions are met: 


¢ You’re adding characters to the end of the layout shape. 
¢ There’s a single run of text within the layout shape. 


¢ The shape remains on the same device. 


If yow’re drawing non-hairline geometric shapes and want to get them on the 
screen as fast as possible, you can set the gxCacheShape attribute of the shape. 
With this attribute set, QuickDraw GX will preprocess all the required parts of 
a shape into compressed bitmaps. This means the shape is completely ready to 
be drawn when you call GXDrawShape: the bits are just blasted to the screen. 


Any time you change a view port’s clip or mapping, QuickDraw GX will update 
all of the caches for the shapes and child view ports associated with that view 
port. There isn’t any speed advantage to setting the clip or mapping of a shape 
rather than the clip or mapping of the shape’s view port; the same work has to 
happen in either case. Note, though, that if the view port contains other shapes 
or has child view ports, their caches will be invalidated, too. You should change 
the view port’s mapping to move a shape only when you want all shapes within 
the view port to move the same amount. 


I’m looking into the possibility of writing a QuickDraw GX printer driver that will 
only print to a file, and I've run into a couple of stumbling blocks I hope you can help 
with. First, is there a way to create a desktop printer from the Chooser without having 
an actual printer attached or selecting a port? Can I get to the Chooser list so that I can 
display some kind of dummy or ghost printer? The second issue has to do with the user 
interface of the Print dialogs/panels. I would like to set the “Destination: File” radio 
button and disable the “Destination: Printer” one for QuickDraw GX application 
interfaces. 


There should be no problem creating a printer driver that only prints to a file. 
And yes, users will be able to create a desktop printer from such a driver. You 

can access the Chooser list: for an example of this, look at the source code file 
Chooser.c from any of the QuickDraw GX printer driver samples. You can do 


whatever you want within the Chooser to handle and display lists of available 
printers for the currently selected printer driver. You’ll also need to modify the 
‘comm' resource to make sure that the Chooser does the right thing. You 
should be able to put a “nops” 'comm' resource reference into the 'look' 
resource. 


You can disable any item in the Print dialog by overriding the GXJobPrintDialog 
message and, in your override, getting the destination tag for the appropriate 
item and locking it. This will make the item appear disabled in the dialog. 
PrintingMer.h contains all the tags you can lock. You should be sure to set up 
the item to be gxVolatileOutputDriverCategory so that your settings go away if 
the user switches drivers in the pop-up menu; you can simply OR this with 
collectionLockMask when you set the item’s attributes. 


The documentation seems a bit thin on what resource type/ID is needed for ColorSync 
profiles in QuickDraw GX printer drivers. Is the appropriate type ‘prof’ or ‘cmat'? 
Will QuickDraw GX automatically use one if I stick it in my driver, or do I also have 
to override the various profile messages? 


All you need to do is include a 'cmat' resource with ID gxColorMatchingDataID 
and specify it in your 'rdip' resource. However, you should also override 
GXFindFormatProfile so that inquiring applications can ask you what the 
format’s profile will be. Additionally, you should override the GXImagePage 
message if you want to provide different profiles on a page-by-page basis. 


P’'m working on QuickDraw GX printer drivers. Can you give me some information on 
what must be done to support PrGeneral? 


PrGeneral support within your QuickDraw GX printer driver is automatic. The 
printing system will automatically maintain the appropriate information within 
the QuickDraw GX print record. The only exception to this automatic support 
is the SetRsl opcode: if you don’t want QuickDraw GX to use the default 
gxResl Type resource when the SetRsl opcode is used, you need to define a 
gxResl Type resource of your own that reflects the capabilities of your printer. 


Our application generates its own custom PostScript™ code when printing to PostScript 
printers. We'd like to support QuickDraw GX-style printing and still be able to 
continue generating our own custom PostScript code to send directly to the printer. 
What's the best way to handle this? I've tried generating empty shapes with custom 
PostScript code attached as a tag (using synonyms), but the LaserWriter QuickDraw 
GX driver emits its own “wrapper” code around our custom PostScript code, which 
could alter the printer’s graphics state. Are there any other methods to achieve this 
without the possible side effects? 


You're actually very close to sending PostScript code correctly through the 
QuickDraw GX printing system without the side effects. As you already know, 
after a shape with 'post' tags has been sent to a PostScript printer, QuickDraw 
GX performs a PostScript restore. There’s no way to prevent this from 
happening. However, only shape objects cause this behavior; QuickDraw GX 
will not perform a PostScript restore for any other object besides a shape. 


The fastest and best method for sending PostScript code is to attach the code 
with tags (using synonyms) to only one empty shape. You can attach as many 


MACINTOSH Q&A 


101 


1 02 develop Issue 19 September 1994 


tags as are required. We recommend that the tags contain 12K of PostScript 
data each, for optimal performance. If you’re sending PostScript code down to 
replace the clip or ink or some other object besides a shape, just attach a 'post' 
tag to the object your PostScript code describes; in this case a restore will not 
occur. 


How should I download fonts to a PostScript printer under QuickDraw GX? I’m 
sending a direct stream of custom PostScript code, but I don’t expect the driver to be able 
to deduce which fonts need to be included. I’ve read about the PostScript control tag that 
can be attached to a shape, and I know the structure for the tag contains font 
information, but the documentation about this tag is sketchy. Can you provide more 
details? 


Related to this question, what’s the best way to cause fonts to be downloaded under the 
current Printing Manager? We’re using the “draw a single character off the page in 
the proper font” trick. I understand this practice is frowned on. Is there an approved 
way to do this short of intermixing QuickDraw and PostScript code in PicComments? 


Our thinking has changed regarding the use of the PostScript control tag for 
downloading a font. If you want to download a font within your PostScript 
stream, you should call GXFlattenFont and pass the font within your 'post' tag. 
The GX PostScript engine will unflatten the font and download it when it finds 
it attached to your ‘post’ tag. 


The method you’re using (drawing a character off the page) is completely 
supported under QuickDraw GX. It was the recommended method for a long 
time. However, the current recommendation is to use Draw Text and pass in text 
that contains all the glyphs you want to use. The reason this approach is better 
for QuickDraw GX is that only the required information is downloaded, 
thereby saving memory on the printer. 


Do I need to override the GXFreeBuffer message in my QuickDraw GX printer driver 
if I perform a total override of the GXDumpBuffer message? If I do need to override 
GXFreeBuffer, what do I do with the buffer? Should I dispose of it with DisposePtr? 


‘The documentation is a little confusing about the purpose of the GXFreeBuffer 
message. GXFreeBuffer is sent to ensure that the indicated buffer has been 
processed and is now available for more data; it doesn’t actually dispose of a 
buffer. The only time you need to override GXFreeBuffer is if you’re doing 
custom I/O (the customl0O flag is set in your 'iobm’ resource). The default 
implementation of GXFreeBuffer will work correctly for QuickDraw GX’s 
default buffering mechanism. 


GXFreeBuffer allows you to start asynchronous I/O in GXDumpBuffer; 

then other buffering routines can be sure that operations on a buffer are 
completed before they reuse the buffer (or dispose of it). If you’re overriding 
GXFreeBuffer, you should just hang out in your override until I/O has 
completed for the buffer passed, and then return. If you’re doing synchronous 
I/O, just return immediately, since all I/O must have completed. 


When I control the start of a QuickTime movie from within my application, the movie 
controller doesn’t get updated properly. I’m calling StartMovie to begin the movie as 
soon as it becomes visible, and ’'m updating the movie controller like this: 


theResult := MCDoAction(tmpMovie.fController, mcActionPlay, @curRate); 
However, this doesn’t seem to work. What am I doing wrong? 


The MCDoAction call with mcActionPlay doesn’t take a pointer to the data in 
the last parameter; it takes the data itself. But since the prototype specifies type 
(void *), to make the compiler happy it must be cast to a pointer. Many people 

have fallen into this trap. 


‘The recommended method to start a movie when you’re using the standard 
movie controller component is as follows: 


// Play normal speed forward, taking into account the possibility 
// of a movie with a nonstandard PreferredRate. 

Fixed rate = GetMoviePreferredRate(MyMovie) ; 
MCDoAction(MyMovieController, mcActionPlay, (void *)rate); 


If you do need to use StartMovie, the correct way to cause the movie controller 
to update is to call MCMovieChanged. 


We’re using the Communications Toolbox in our application and have noticed some 
strange behavior. Specifically, when a tool is being used, the tool’s resource fork is 

placed not at the top of the resource chain, but behind the System and application 
resource forks. As a result, we’re having trouble with tools that use STR# resources that 
conflict with those in our application. This results in bad configuration strings or 
configuration dialogs with the wrong text. I can easily work around this by changing 
the resource IDs in my application, but this doesn’t solve the problem in the long run. 
Any advice? 


What you're seeing is a part of the “pathology” of the Communications 
‘Toolbox and its tools. Both do a less than perfect job of looking in the correct 
resource map for string resources. The result is what you’re currently seeing: 
application strings end up being placed where tool strings are expected. 


There are a couple of workarounds. The first you’ve cited, which is to make 
sure that none of your application’s resource ID numbers conflict with the CTB 
tool’s ID numbers. However, as you also noted, this can be a problem when 
you're using several CTB tools, and may not work if the user happens to select a 
tool that has a conflicting ID. 


The better way to work around the problem is to save a reference to the current 
resource file and then set the resource file to the System file. After completing a 
call to CMChoose or CMGetConfig, you can reset the resource file to the one 
you started with. Here’s how to do this: 


short oldResFile; 
Point dlogBoxPt; 
Ptr myConfigString; 


/* First save the current resource file. */ 

oldResFile = CurResFile(); 

/* Now set the resource file to the System file. */ 
UseResFile(0); 

/* Next call CMChoose to configure your connection tool. */ 
myErr = CMChoose(&myConnectionHdl, dlogBoxPt, nil); 


MACINTOSHQ&A 103 


1 04 develop Issue 19 September 1994 


/* Now call CMGetConfig to get the configuration string from the 
connection tool. */ 

myConfigString = CMGetConfig(myConnectionHd1) ; 

/* And finally, restore the old resource file. */ 

UseResFile(oldResFile); 


What is the Human Interface suggestion for removing a digital signature from a 
signed document? I see how to add and how to verify, but I can’t find any suggestions 
for removing signatures. 


Here’s the relevant paragraph from the AOCE Human Interface Guidelines 
document, which can be found on AppleLink (search the AOCE Talk folder): 


Signatures may also be deleted by users. To accomplish this, the user 
should select the signature by clicking on its icon and choosing Clear 
from the application’s Edit menu. Selecting the signature icon and 
pressing the delete key is a desirable alternative. Note that signatures 
must not be cut, copied, or pasted. 


To actually remove a signature, just remove the 'dsig’ resource from the file. 
Note that signed files may have the Finder “locked” bit set. If you remove the 
signature, you should also clear this bit. 


I launch my application by dragging files onto its icon. It then opens the files, performs 
some quick operation, and quits. I can put '****' and ‘fold’ resources in FREFs to let 
users drag any file or folder onto the application icon. But when a user drags an AOCE 
catalog (or anything inside the catalog) onto the icon, the Finder won't let the user drop 
it onto my application. What do I need to do to my application to let users drop-launch 
it with AOCE catalogs (or their contents)? I know it’s possible: the “Find in Catalog” 
application will drop-launch if a user dragged to it from a catalog. 


If you look, you'll see that “Find in Catalog” is a Catalogs Extension template. 
It’s not an application program; it actually executes as part of the Finder. 
Unfortunately, you can’t do what you want to with an application. You 

might want to look at the Catalog Service Access Modules chapter in Inside 
Macintosh: AOCE Service Access Modules for more information on the Catalogs 
Extension. 


When users launch my utility application by double-clicking, I present a Standard File 
dialog that lets them choose a file to operate on. I'd like them to be able to browse 
AOCE catalogs as well as HFS files, but catalogs don’t show up in the Standard File 
dialog. I could use another dialog for browsing AOCE catalogs, but why use two 
different interfaces for the same action (from the user’s point of view, that is; the user 
just wants to specify an object, wherever it may be)? Is there a way to get catalogs to 
show up in the Standard File dialog? Is there any way to browse the file system and the 
AOCE catalog system in the same dialog? If the answer is no, is there an analog to 
Standard File for AOCE catalogs? 


You can’t browse HES files and AOCE catalogs in the same dialog, since they’re 
two different file systems. To let the user browse the AOCE catalog system, you 
need to use the AOCE Standard Catalog Package Reference routines, which are 
documented in Inside Macintosh: AOCE Application Interfaces. There is a routine 
that’s analogous to StandardGetFile. The AOCE Software Developer’s Kit 


(available through APDA) includes sample code that shows how to browse 
AOCE catalogs. 


Pm writing an application that will watch a user-specified folder and operate on files or 
folders that are dropped into it. I need to perform operations on every item in the folder 
and its subfolders. What will happen if I begin to walk through a new folder with 
PBGetCatInfo while the Finder is still copying files into the subfolder structure? Can I 
guarantee that I'll detect all the files? 


There’s no way to find out directly when the Finder is done copying items into 
the folder, but there is a strategy we can recommend. First, though, you should 
know that the recommended way to determine whether items have been added 
to a folder is to watch its modification date. So how can you know when all the 
files have arrived? The recommended strategy is to poll the parent folder’s 
modification date every few seconds after you first detect a change, and keep 
polling until you have a reasonably long interval during which there’s no change 
in the date (30 seconds is probably about right). This means, of course, that 
there’s a delay before you act on files dropped into the folder, but as the Print 
Monitor shows us, this delay is probably reasonable to the user. 


One more possible gotcha you should know about: to tell whether it’s safe to 
work with a particular file, it’s not enough just to make sure the file isn’t open; 
you need to check its length. Ifa file is closed (that is, not busy) but has no 
length in either its resource or data fork, you caught the Finder at an 
uncomfortable time, after it created the file but before it opened it. So to know 
if you can operate on a file the Finder might be copying, you must determine 
two things: it’s not already open, and it has length in its resource or data fork. 


Our application uses the Icon Utilities interface, and we want to verify that these 
features are present before we use them. We’ve been unable to do this successfully. The 
Macintosh Technical Note “Drawing Icons the System 7 Way” (QuickDraw 18) doesn’t 
say how to do this, but Inside Macintosh: More Macintosh Toolbox recommends using 
Gestalt with the gestaltIconUtilities selector. When we try this, Gestalt returns an error 
of -SSS1 (undefined selector). What are we doing wrong? 


There isn’t a Gestalt selector for the Icon Utilities; Inside Macintosh and the 
header files are wrong. Even if we were to correct that situation tomorrow (or 
in the next system software release), it wouldn’t help, since the Icon Utilities are 
available on systems where the Gestalt selector isn’t. The solution is to use the 
‘TrapAvailable function to see if the _IconDispatch A-trap is available. You can 
find the source code for TrapAvailable in Inside Macintosh Volume VI on page 
3-8, or in Inside Macintosh: Overview on page 180. 


I have a System 7 application that the user can drag files, disks, or folders to. How can I 
determine from the Apple event information which type of item (file, disk, or folder) has 
been dragged to the application icon? 


When the user drags a file, disk, or folder to an application icon, the Finder 
uses the Process Manager to open the application and then sends it an Open 
Document (‘odoc') event containing a list of alias records for each object 
dropped. When your application receives the event, it needs to open each of 
the objects specified in the event by getting each alias record from the list and 
coercing the data for that record to an FSSpec. Once you have the FSSpec, you 


MACINTOSH Q&A 


105 


1 06 develop Issue 19 September 1994 


can check its parID to determine whether the directory ID is fsRtParID, 
indicating that you’re looking at a volume. If the parID is anything other than 
fsRtParID, use PBGetCatInfo to determine if you have a file or a folder. Here’s 
the code: 


enum {kItsAVolume = 1, kItsAFolder, kItsAFile}; 
pascal OSErr GetSpecType(FSSpec *myFSS) 


{ 
CInfoPBRec pb; 
OSErr myErr; 
short objType = 0; 
if ((myFSS->parID) == fsRtParID) 
objType = kItsAVolume; 
else { 
pb.hFileInfo.ioNamePtr = (StringPtr) *(myFSS).name; 
pb.hFileInfo.ioVRefNum = *(myFSS).vRefNum; 
pb.hFileInfo.ioDirID = *(myFSS).parID; 
pb.hFileInfo.ioFDirIndex = 0; 
myErr = PBGetCatInfoSync(&pb) ; 
if (myErr == noErr) { 
/* Check to see if bit 0x10 of ioFlAttrib is set; if it is, 
we've got a directory */ 
if ((pb.hFileInfo.ioFlAttrib & 0x10) != 0) 
objType = kItsAFolder; 
else 
objType = kItsAFile; 
} 
} 
return (objType); 
} 


Q [I'm trying to implement “the perfect component.” The goals for this component are fast 


dispatching, delegatable, able to rely on delegates, ready for everything, and surprised 
by nothing. I've been using develop Issues 12 and 14 and Inside Macintosh: More 
Macintosh Toolbox to guide me, but I still have a question that wasn’t addressed in those 
references. 


Pm using the Fast Dispatch method to dispatch my component’ calls. Pve figured out 
how to repair the stack after getting an unsupported routine selector code, but I can’t 
figure out how to delegate a call. I think ’'m recovering the stack correctly, but all the 
documentation I’ve read doesn’t even hint at this sort of functionality. I was thinking 
that I could use DelegateComponentCall after creating a ComponentParameters record, 
or I could calculate the size of the parameters and attempt to set up the stack for a 
ComponentCallNow call. However, neither of these is a good solution — they both take 
too many instructions to implement and obviate the advantages of fast dispatching. Is 
there such a thing as a DelegateFastComponentCall that I haven’t heard of? 


‘Try the following to delegate a component call: 


move.l d0, -(sp) 
PUSHDELEGATEGUY 
move.q  #-2,d0 
dc.w $a82a 


; push dO onto stack 
; macro to push component instance 


What apparently happened is that the engineers realized how difficult it was to 
call DelegateComponentCall when the stack was screwed up. So they created a 
“special” delegate call. As you can see, the selector is -2. This is supposed to be 
documented somewhere in the Component Manager documentation, but was 
inadvertently left out. 


Q Why aren’t my components getting register calls? I've set the appropriate flag. I'm 
registering the component from my application. 


A Your component won’t get called to register at all since it’s being registered by 
an application. If you removed your component and placed it in the Extensions 
folder so that it was registered at system startup, it would receive a register call. 
The reason for this is that registration of components happens only when the 
Component Manager is first loaded at system startup. After system startup, 
components can be registered by applications, but the register routine will not 
be called. 


Q I heard that the new Apple digital camera will introduce a graphics format called 
QuickTake. Is this truly a new format or are these just Quick Time compressed PIC'Ts? 
If it’s a new format, where can I find documentation on it? 


A QuickTake stores its pictures as compressed PICT files. There’s a new CODEC 
that’s necessary to decompress the images, but no new file type. The QuickTake 
100 Digital Camera Developer Note contains extensive information about 
talking to the camera from both Macintosh and Windows machines. It also 


documents the picture formats. The Quick’ Take Camera Software Development 
Kit is available from APDA. 


The QuickTake application that comes with the camera can save the pictures in 
a variety of formats, including PICT (with or without various compressions) 
and TIFF. There’s also a control panel that allows the user to mount the camera 
as a read-only serial RAM disk (similar to MountImage), so your application can 
directly download the information from the camera’s memory. 


Q Pm writing my first action atom for the Installer. I tried writing a skeletal example, as 


follows: 
resource 'inaa' (30000) { 
format2 { 
continueBusyCursors, actAfter, dontActOnRemove, 
actOnInstall, 
‘infn', 149, 
0 ' 
1000, 


"Delete Folder" 
bhi 


#include "ActionAtomHeader.h" 
ActionAtomResult ActionAtomFormat2(ActionAtom2PBPtr aa) 
{ 


Debugger(); 
return (kActionAtomResultContinue) ; 


MACINTOSHQ&A 1Q7 


1 0s develop Issue 19 September 1994 


But the action never gets called! I don’t have to explicitly refer to the 'inaa' anywhere, 
do I? I checked that the resources were copied into the file correctly and had the right 
IDs. What am I doing wrong? 


Your action atom is fine. The only mistake you’ve made is not tying your ‘inaa' 
to a package (‘inpk'). You have to add to your ‘inaa' a reference to an active 
‘inpk'. Once you do that, it works great. (We had trouble with this one until we 
found a cool diagram on page 8 of the Installer documentation that shows how 
the script resources interrelate; that diagram is your friend, and seems to 
encapsulate quite a bit of critical information.) 


We’re planning a zone name change for the zone that contains some of our PowerShare 
servers. Will this require any action on the part of the administrators of the various 
servers beyond informing users of the change? 


It depends on whether you’re changing the zone names of some or all of the 
servers. If you’re changing the names of only some servers, it’s entirely possible 
that you don’t have to notify anybody, depending on how you’ve replicated your 
folders. For a while the new servers won’t be contacted by any clients since all 
clients will try to address them at their old location, but eventually all servers 
will again start receiving requests from clients. 


If you’re changing the zone names of all the servers, the client software won’t 
recognize that the zone names have changed, so you'll have to throw away your 
key chain and add it again after the zone names have changed. The PowerShare 
servers will eventually recover and rediscover all the other servers. We 
recommend that you bring up the Master Pathfinder first after changing its 
zone name and then bring up all the other servers. Once all the servers are up, it 
should take a few hours at most for everything to settle down again and for the 
system to be purring. 


The function SMPEnumerateBlocks takes a buffer that returns information about the 
blocks in a letter. The documentation (Inside Macintosh: AOCE Application Interfaces, 
page 3-87) says that the buffer contains “a count byte indicating the number of blocks in 
the letter, followed by a block information structure for each block.” I can’t find 
anything that tells me what a “block information structure” is. 


It looks to me as if it actually returns the count of blocks in a short, not a byte. Is this the 
total number of blocks, or the number of blocks returned by the call? If I have to call the 
function again to get information that didn’t fit in the buffer, do I get another count 
followed by more block information structures, or do I just get an array with more block 
information structures without the count (short)? 


The block information structure seems to be 16 bytes long, starting with an 
OCECreatorType structure. Is this always correct, and what is the other information? 


This is somewhat confusing. The “block information structure” referred to here 
is the MailBlockInfo structure defined in the OCEMail.h header file: 


struct MailBlockInfo { 
OCECreatorType blockType; 
unsigned long offset; 
unsigned long blockLength; 


And yes, the count byte returned in the buffer you provide is actually a short 
(the documentation is wrong). This value is placed right in front of the first 
MailBlockInfo structure each time you make the call. The count indicates the 
number of blocks that were put in your buffer for this particular call (not the 
total), which of course depends on the size of the buffer you pass; it fits in as 
many as it can. Check the “more” parameter to see if your buffer was too small 
to hold all the blocks, and check the nextIndex parameter for the sequence 
number of the next item to be returned. 


Q I found what I think is a problem with the TEGetOffset routine: when it returns, a 


28-byte handle has been locked for single-styled text. I believe it’s a style handle that gets 
locked. This seems to be an intermittent bug, occurring only about half the time. Is there 
a workaround? Is it safe to just unlock the style handle after calling TEGetOffset? 


It’s a bug all right, and it is the style handle that’s getting locked. Here’s a 
workaround: 


static short MyTEGetOffset(Point pt, TEHandle th) 
{ 

TEStyleHandle sh; 

short theResult; 

char saveState; 


if (th == 0L) 

return; 
sh = GetStyleHandle(th) ; 
saveState = HGetState(sh); 
theResult = TEGetOffset(pt,th); 
HSetState(sh, saveState) ; 
return theResult; 


Q I have a question about the Japanese art of bonsai (colloquially known as stunting 


A 


trees): What happens if you bonsai a fruit tree? Does the fruit come out dwarfed as 
well? Or does the tiny tree produce full-size fruit? 


This was a difficult one to find an answer to. It seems to depend on many 
factors, not the least of which is the kind of fruit tree you're talking about. One 
person we talked to said he once saw a bonsai lemon tree nine inches tall, with a 
single, full-size lemon hanging from a branch. (Actually, the lemon didn’t hang; 
it would have broken the poor tree. Instead it rested on a small platform built 
especially for that purpose.) But there were also reports of a stunted crabapple 
tree that produced apples the size of peas. Go figure. 


These answers are supplied by the 
technical gurus in Apple’s Developer Support 
Center. Special thanks to Pete (“Luke”) Alexander, 
Joel Cannon, Mark (“The Red”) Harlan, Dave 
Hersey, Dave Johnson, Don Johnson, Scott 


Kuechle, Jim Luther, Kevin Mellander, Jim 
Mensch, Martin Minow, and John Wang for the 
material in this Q & A column. If you need more 
answers, take a look at the Macintosh Q& A 
Technical Notes on this issue’s CD.°® 


MACINTOSH Q & A 


109 


THE VETERAN 
NEOPHYTE 


Rubber Meets 
Road 


DAVE JOHNSON 


I’ve been thinking about edges lately — about the 
places where dissimilar domains meet and interact. You 
know how every now and then you come up with a new 
view on things? A new model to try to fit the facts into, 
a new lens to use to examine the world, a new pattern 
that you haven’t noticed before but that suddenly seems 
pervasive? Edges are like that right now for me. It 
seems that everywhere I look I see edges, and the edge 
always seems to be where the action is. 


I think it started in January, when I was called for jury 
duty. I was promptly selected to serve on a long, 
complex, and sordid criminal trial. ’ve been called for 
jury duty only once before, and that time the 
experience was short and dull. I did serve on two juries, 
but neither trial lasted more than a couple of days, and 
they were both very mundane. This time was decidedly 
different. There were 4 defendants, 53 separate counts 
to decide, 3 different crime scenes, dozens of spent 
bullet casings and slugs and shotgun waddings to keep 
track of, something like 14 police witnesses and 6 or 8 
civilian witnesses, a two-inch thick stack of 8 by 10 
color glossies, and lots more. The whole adventure 
took nine weeks to play out. Yow. 


‘The atmosphere in the courtroom spanned the full 
range of intensities. There was plenty of plodding 
boredom: day after somnolent day of slow, thorough, 
painstaking ballistics testimony, matching bullets to 
guns and mapping where they were found. There was 
high drama: the tapes of the police transmissions 
during the chase and as the final shootout began were 
filled with panic, screaming. There was humor: Helen 
in chair 5 often started to fall asleep in the afternoons. 
The court reporter would see her dropping off, make a 
little hissing noise, and Wes in chair 4 would 
surreptitiously nudge Helen back to consciousness. 
We'd all grin. 


But no matter what was happening at the moment, I 
found the process absolutely riveting, from beginning to 
end. Here were the great and mighty wheels of justice 
in America, slowly and ponderously turning, grinding 
away at the facts like so much dry corn under a 
millstone. The courtroom is a place where politics 
actually collides directly with people’s lives, through the 
strange intervening filter called law. It’s an edge, an 
active boundary separating two domains, where work 
actually gets done. 


I’m constantly drawn to active boundaries like that, 
places where two dynamic systems collide and affect 
each other. Interfaces. Precipices. Limits. Edges. And 
they are everywhere. In a previous column I pointed 
out an edge in the realm of language: semantics, where 
a language’s structure collides with meaning and where 
the real work of the language — creating meaning from 
abstract symbols — gets done. In biology, there are 
edges all over the place. An obvious and important one 
is the semi-permeable membrane. It’s the structure that 
allows life to create and control its own environment, 
and it’s arguably the single most important structure 
enabling complex multicellular life to exist. Biologically 
active molecules are active because of their shape, their 
boundaries; proteins and enzymes work because they fit 
together with complementary molecules. In philosophy 
there is the edge between self and not-self, and 
teetering along this edge, hopping back and forth 
across it and trying to look at it from all angles, is how 
the work of philosophy gets done. In physics, often the 
edges are where the truly interesting — and, not 
coincidentally, mathematically intractable — stuff 
happens. (In engineering school, an all-too-common 
phrase was “ignore edge effects.”) 


All the exciting stuff seems to happen at edges. Large 
systems that incorporate feedback often exhibit a 
behavior known as “self-organized criticality” in which 
they evolve toward a critical state, an edge, and 
forevermore exist there, teetering on the crumbling lip 
of stability. A great example is a conical pile of sand on 
a circular plate, with grains being added to the top one 
at a time. Over time the overall shape of the pile will 
change very little, but if you turn up the magnification 
and look closely at the side of the pile, there are 
constant avalanches of all sizes, all extremely 
unpredictable and chaotic. This is an interesting dual 
behavior: at one scale there is incredible robustness; the 
overall shape of the pile is very stable and will always 
recover itself, even if disturbed. But on a smaller scale, 
the scale of an individual grain on the side of the pile, 
the dynamics are wildly unpredictable and incredibly 


DAVE JOHNSON likes to try to slip new words he’s learned into 
casual conversations without anyone really noticing. Two years 


110 develop Issue 19 September 1994 


ago he learned the word enantiomorph. As you might imagine, 
he’s still waiting for the right opening. ® 


unstable. The pile is poised at a limit, a dynamic 
balance between growth and decay. 


An interesting thing is how many different varieties of 
dynamic systems seem to exhibit this kind of behavior. 
The locations and magnitudes of earthquakes, 
fluctuations in traffic flow, the rise and fall of economic 
markets, the rhythmic variations in a heartbeat, the 
varying current through a resistor, and the population 
changes in an ecosystem all exhibit dynamic 
characteristics similar to the sand pile, and this is not an 
exhaustive list by any means. That state, pushed up 
against the edge of stability, seems to be a natural one. 
Life itself appears to be delicately poised on the 
boundary between order and chaos. 


In computers (you knew I was going to get around to 
this eventually, didn’t you?), as in any complex system, 
there are lots of interesting edges and boundaries if you 
look for them. Internally, there’s the place where the 
software collides with the hardware; sparks really fly 
down there, all right. Object-oriented programming is 
all about repackaging the boundaries between and 
among data and functions. (A large part of good object 
design is minimizing the “surface area” of your objects.) 
And then there’s the edge of the computer itself. And I 
don’t mean the plastic or metal surface of the box, but 
the experiential boundary, the true edge between the 
machine and the user, the interface. Here the animal 
collides with the machine, and the boundary between 
them is infinitely convoluted, elastic, dynamic, and 
interesting. 


For software designers, perhaps the most important 
lesson to be learned from the edge-centric view is this: 
the shape of a boundary defines the shape of things on 
both sides of the boundary simultaneously. The 
boundary of my dog Natty defines not only her own 
shape, but that of a Natty-shaped hole in the air as well. 
The edge between two interlocking tiles in an Escher 
drawing defines the shape of both tiles at once. If the 
edge in question is one we have control over, this can 
be very important. 


By programming a computer we’re not only shaping 
the machine; we’re also shaping the humans who use it. 


This is often overlooked, but is crucial to designing 
good software; it needs to fit. Humans are incredibly 
adaptable, and will contort themselves grotesquely to 
use awkward tools, if necessary. Like kids with their 
faces squashed against the toy store window, computer 
users smash themselves up against the interface — even 
though it might hurt — to get at what’s inside. 


But because of the chameleon-like nature of the 
computer, we have more or less total control over the 
interface. So in principle we have the power to shape 
the computer to the user, rather than the other way 
around. We should be able to make a truly human- 
shaped dent in the computer, a dent people can slip 
into effortlessly and comfortably, like slipping into a 
fuzzy slipper. It’s incredibly hard work, shaping the 
computer to the human, all that snipping and tucking 
and smoothing. It requires constant readjustment, 
painstaking attention to detail, and massive amounts of 
brute-force trial and error. But it’s good work, some 
would say the work that humans are best at: the 
shaping of tools. 


So now here I am, seeing edges everywhere. Sigh. Last 
year it was basins of attraction, this year it’s edges, next 
year maybe it’ll be networks of interconnections. But 
there’s one thing I can count on: every time I get tired 
of looking through one particular glass, there will be 
another within reach. Humans have this uncanny 
ability to apply order to everything they see, to perceive 
structure in everything around them. Our minds seem 
to operate by forming and then reforming meaning, 
establishing and then reestablishing context, constantly 
slipping and adjusting to accommodate the relentless 
stream of input. Hmm. Just like that pile of sand. 


RECOMMENDED READING 


¢ Complexity: The Emerging Science at the Edge of 
Order and Chaos by M. Mitchell Waldrop (Simon 
& Schuster, 1992). 


¢ How Dogs Really Work! by Alan Snow (Little, 
Brown and Company, 1993). 


Thanks to Jeff Barbose, Michael Clark, Michael Greenspon, 
Brian Hamlin, Mark (The Red”) Harlan, Bo3b Johnson, Lisa 
Jongewaard, and Ned van Alstyne for their always enlightening 
review comments. ® 


Dave welcomes feedback on his musings. He can be reached 
at JOHNSON.DK on AppleLink, dkj@apple.com on the Internet, or 
75300,715 on CompuServe. ® 


THE VETERAN NEOPHYTE 


Newton 
Q&A: 


Ask the 
Llama 


112 develop Issue 19 September 1994 


Q I'm having trouble with the protoRoll. I have a protoApp with a protoRoll at the bottom 
with a couple of items in it. (Note that ’'m not using the protoRollBrowser proto.) It 
compiles OK, but when I download to the Newton, nothing shows up. In fact, when I 
use the inspector to look at the view hierarchy, the protoRoll doesn’t show up at all. The 
other views are fine. What am I doing wrong? 


A The protoRoll doesn’t show up because of the setting of the viewF lags of the 
ROM prototype: the vApplication and vClipping flags are set, but not the 
vVisible flag. If the protoRoll were the base template of your application, the 
vApplication flag would be sufficient to make it visible. 


In your case, the protoRoll is a child of your base application template. Since it 
isn’t visible (vVisible isn’t set), the system doesn’t create a runtime view frame 
for the child. You could get the system to create the runtime view by declaring 
the protoRoll to be the base template, but this still wouldn’t show the protoRoll. 


‘To make the protoRoll visible, add a viewF lags slot to the protoRoll and check 
the vVisible flag. You may or may not want to uncheck the vApplication flag. If 
you uncheck it, the system will no longer send scroll and overview messages 
(viewScrollUpScript, viewScrollDownScript, viewOverviewScript) to the 
protoRoll, so it will appear to be broken. But you can support these messages in 
your base application view and just pass them on to the protoRoll as needed. If 
you leave the vApplication flag checked, protoRoll will get the scroll events. 


Q My print format never seems to get called, ever. I don’t get a printNextPageScript or 
even a viewSetupFormScript. P’'m not using ROM_coverPageFormat because I don’t 
ever want to print a cover page. How can I get this to work? 


A The answer to your problem is in your question. A print (or fax) format must 
proto to ROM_coverPageFormat; it’s not optional (as the manual implies). It may 
help to know that ROM_coverPageFormat is really misnamed. The generation 
of a cover page is controlled by a slot in your format. The proto should be 
called something like ROM_allThePrintingAndFaxingBehaviorProto, but that 
would be verbose :-) 


Q I would like to add a [button | view | Llama] to the [Notepad | Calendar | Cardfile| etc.]. 
How can I do that safely? 


A Thisisa simple one: you can’t. If you add any element to a built-in application, 
you take the chance that your application will break in future releases of 
MessagePad. Also note that adding llamas to MessagePad will theoretically 
cause a multidimensional implosion. (“Don’t cross the llamas, er . . . beams.” — 
LlamaBusters) 


Q Pve noticed some peculiar behavior in the Compile function and am wondering if it 
might be a bug. The problem is with special characters and string objects. When 
Compile is passed a string object containing special characters rather than a literal 
string with Unicode codes, the result is incorrect. This example works as expected: 


The llama is the unofficial mascot of the NewtonMail DRLLAMA or AppleLink DR.LLAMA. 
Developer Technical Support group in Apple’s The first time we use a question from you, we'll 
Personal Interactive Electronics (PIE) division. send you a T-shirt.® 


Send your Newton-related questions to 


X:= Compile("{msg: \"A string with special character \u00A5\u\"}"; 
yi= ix(); 


—> y is {msg: "A string with special character ¥"} 


This example doesn’t work as expected: 


a:= "A string with special character \u00A5\u"; 
X:= Compile(a); 
yi= ix(); 


—> y is {msg: "A string with special character *"} where * is some character other than 
the expected "¥". 


Can you explain what’s going on here? 


A The problem is that you’re using illegal NewtonScript syntax in the second 
example. If you used the inspector instead of Compile for this example, it would 


be like typing 
A string with special character \u00A5\u 


and then hitting Enter. This would result in a syntax error from NewtonScript. 
What you probably want is the equivalent of typing 


"A string with special character \u00A5\u" 
into the inspector. This is done with the following call to Compile: 


x := Compile("\"A string with special character \\u00A5\\u\""); 
call x with (); 


—> #4415F49 "A string with special character ¥" 


Note that the escape characters (\) for the Unicode string are themselves 
escaped. If you don’t do this, you'll be putting the actual Unicode characters 
into the string being compiled, which is probably not what you want. Although 
your first example worked, you could easily get a case where not escaping the 
escape characters could bite you. 


Q In the communications input spec below, why does the call to UpdateStatus fail? 
UpdateStatus is a method in my base view, and the whole endpoint is in my base view, 
so why can’t the input spec find the method? 


GetMessage: { 
inputForm: 'string, 
endCharacter: unicodeCR, 


InputScript: func(endpoint, data) 
begin 

:UpdateStatus (data); 

endpoint: SetInputSpec(GetMessage) ; 
end; 


} 


A The call to UpdateStatus fails because it’s a message send that uses full 
inheritance to find the method. That means the system will look in the current 


NEWTON @ & A: ASK THELLAMA 113 


114 develop Issue 19 September 1994 


> 0 FO 


context (that is, self), then check the proto chain, and then check the parent 
chain. However, the current context is not what you think it is. In an input spec, 
the current context is the frame that defines the input spec. In this case, it’s the 
GetMessage frame you define. 


Since the GetMessage frame has no proto or parent pointer, the message send 
fails. There’s a second problem waiting to happen: the call to SetInputSpec will 
also fail, because the symbol GetMessage isn’t valid in this context. 


The solution is to get a reference to your base view (or another view that 
contains or inherits the UpdateStatus message). The usual way to do this is to 
add an _parent slot to your endpoint at run time during initialization. Now your 
InputScript can use endpoint._parent to find the base view, as follows: 


InputScript: func(endpoint, data) 
begin 

endpoint: UpdateStatus (data); 

endpoint :SetInputSpec(endpoint.GetMessage) ; 
end; 


If you really want to use a simple message send (for example, :UpdateStatus), 
you could add an _parent slot to the input spec. This may be useful in situations 
where you have several input scripts that rely on a dynamic inheritance 
mechanism. That is, you change what the _parent slot of the input spec points 
to on the fly. 


Did you know that “gullible” is not in the Newton dictionary? 


It is now. 


I have a large amount of static data in my application. I'd like to use Project Data to 
edit this data, but it won't fit. What can I do? 


You must have an old version of the Newton Toolkit. As of version 1.0.1, the 
32K limit is gone. You could use another text editor to edit the Project Data file. 
You could also use the Load command to load another NewtonScript source 
file. 


As an example, assume you had a file called MyData.f in the same directory as 
your project and that this file contained the script that defined your constant 
data structures. You could use the Load command like this: 


// This line appears in your Project Data file. 

// Load in the data file and use the HOME compile-time variable 
// to get the path to the project folder. 

Load(HOME & "MyData.f£"); 


How can I figure out bow much space my package and data will take on a card? I really 
want my application to fit on a 1-meg card. 


The short answer is, you can’t. The long answer is, load your packages and 
soups after completely erasing the card. To completely erase the card, open up 
preferences and then insert the card. Before the card is loaded, you'll get a 
chance to erase it. 


Look at the difference in the free space on the card. Use the value in the card 
dialog. The value in the remove-package picker is the uncompressed size. You 
must erase the card before you check the free space difference. 


I have an input spec that receives data and places it into a queue. When I get data, I set 
a flag in my base view (DatalnQ) that indicates data is available. I know the data is 
getting sent, but my input specs never seem to get called. What’s going on? 


The chances are that your base view has some code like this: 


myBase.WaitForData := func() 
while Not DataInQ do nil; 


You may have more statements in the loop, and you may be using repeat 
instead of while, but you probably have a loop that waits for the DataInQ 
flag to be set. The problem is that you’re not giving control back to the 
NewtonScript thread so that it can process the pending InputScript call (from 
your input spec). 


If you really need to wait for data, you can use either an idle script or a 
repeating delayed action. The idle script will be significantly easier to 
implement. You should make the delay on your idle script long enough to give 
time to the Newton. Also note that the Newton is a battery-powered device, 
and excessive use of this kind of programming tends to drain the users — I 
mean, batteries. 


I have an array of text elements called MyFirstArray in my Project Data file. I want 
to set the text of a clParagraphView that I open to an item in this array. The 
clParagraphView has a slot (strRef) that references MyFirstArray[0]. The first element 
appears as the clParagraph’s view. There are four buttons on the base view, and 
depending on which button is tapped I want a different element of this array to be the 
clParagraph’s text. When I try replacing MyFirstArray[0] in strRef during the 
viewSetupFormsScript, I get as text “MyFirstArray[1]”, not the text this represents. 
Here’s the code in SetupFormScript in the clParagraph: 


SetValue(self, 'strRef, "MyFirstArray["&tempslot&"]"); 


tempslot is a slot in the base view where I store a value depending on which button is 
tapped. What’s the problem? 


The basic answer is that your SetValue statement is incorrect. This statement 
sets strRef to the string “MyFirstArray[” concatenated with the string 
representation of tempslot concatenated with “]”. What you really want is the 
string that’s in MyFirstArray at the position defined by tempslot; that statement 
would be 


SetValue(self, 'strRef, MyFirstArray[tempslot]); 
But there are better ways to do this. Which method you use depends on when 
you set the text of the clParagraphView. If you set up things at open time, use 


the viewSetupFormScript, but just assign directly to the text slot: 


clParagraphView.viewSetupFormScript := func() 
text := MyFirstArray[tempslot]; 


NEWTON Q & A: ASK THE LLAMA 


115 


Remember that SetValue will also dirty the view and call RefreshViews. This 
isn’t something you want to happen when you Open a view. 


The other case is that the clParagraphView is already open. In this case, you can 
use a SetValue statement to set the text slot directly, instead of setting a strRef 
slot. 

One other note: If the user can edit the strings you place in a clParagraphView, 


you must Clone the string. Otherwise you can get a “tried to modify read only 
object” error. 


Q How long does it take to train a llama to be a competent NewtonScript programmer? 


A About four weeks, but the hooves get in the way of really fast coding. 


Thanks to our PIE Partners for the questions Have more questions? Need more answers? 
used in this column, and to jXopher, Todd Take a look at PIE Developer Info on AppleLink. ® 
Courtois, Bob Ebert, Mike Engber, Kent Sandvik, 

Jim Schram, Maurice Sharp, and Scott (“Zz”) 

Zimmerman for the answers. ° 


Do you yearn for the adulation of your colleagues? 


Yearn no more: write for develop. We’re always looking for people 
who might be interested in submitting an article or a column. If 
you'd like to spotlight and distribute your code to thousands of 
developers of Apple products, here’s your opportunity. 


If you’re a lot better at writing code than writing articles, don’t 
worry. An editor will work with you. The result will be something 
you'll be proud to show your colleagues (and your Mom). 


So don’t just sit on those great ideas; feel the thrill of seeing them 
published in develop! 


For Author’s Guidelines, editorial schedule, and information 
YOUR NAME HERE on our incentive program, send a message to DEVELOP on 
AppleLink, develop@applelink.apple.com on the Internet, or 
Caroline Rose, Apple Computer, Inc., One Infinite Loop, 
M/S 303-4DP, Cupertino, CA 95014. 


116 develop Issue 19 September 1994 


KON & BAL’S PUZZLE PAGE 


Heaps of Fun 


See if you can solve this programming puzzle, presented in the form of 
a dialog between Konstantin Othmer (KON) and Bruce Leak (BAL) 
— and a special guest, developer Steve Newman. The dialog gives clues 


to help you. Keep guessing until you’re done; your score is the number to 
Be & & 7 J 


the left of the clue that gave you the correct answer. Even if you never 


run into the particular problems being solved here, you'll learn some 


valuable debugging techniques that will help you solve your own 


programming conundrums. And you'll also learn interesting Macintosh 


trivia. 


Steve 


KON 


Steve 


KONSTANTIN OTHMER, 
BRUCE LEAK, 


AND STEVE NEWMAN BAL 


Steve 


KON 


I’ve got a machine that crashed into MacsBug. I think it’s this bug that 
some of our beta testers have been reporting; it’s rea/ly intermittent, so 
I may not get it to happen again. I’ve got to find it just by looking at 
this one crash. 


It’s not reproducible? 


Not if it’s the bug I’ve been hearing about. The reports are always the 
same: The machine crashes while saving a file. Afterward the file is 
unreadable. If they go back to an older copy of the file, the problem 
doesn’t recur. No single user seems to have had this crash happen 
more than twice, and no one has been able to associate it with 
something they were doing in the program before they told it to save. 


What does this program do? 


It’s a PIM — personal information manager. Data entry and dialog 
boxes and stuff. It’s a pretty big program, but very vanilla in its use of 
the ROM — strictly Volume I stuff, plus the Memory Manager and 
File Manager, of course. 


You’ve tried stress testing? Heap scramble, low-memory conditions, 
MemHell, QC, all of that? 


KONSTANTIN OTHMER AND BRUCE LEAK 
have given up sleep because they need all the 
time they can get to manipulate the penny stock 
market via the budding information superhighway. 
They're no longer trying to break the sound 
barrier, but are working on the Hedgehog 
barrier. BAL wonders, “What's that blue 
Hedgehog got that our green Armadillo doesn’t 


havee”® 


STEVE NEWMAN (AppleLink STEVENEWMAN)} 
has been programming on the Macintosh since 
1984. Currently, he works at Common Knowledge, 
Inc., writing information management tools. In a 
previous life he cowrote FullPaint, FullWrite 
Professional, and Spectre. When asked if he 
thought the Power Macintosh was the hottest new 
game platform he’d seen since the Atari 800, he 


ue 


replied “yes. 


KON & BAL’S PUZZLE PAGE 


117 


100 


95 


90 


85 


80 


75 


70 


11 8 develop Issue 19 September 1994 


Steve 


BAL 


Steve 


KON 


Steve 


BAL 


Steve 
KON 


Steve 


BAL 


Steve 


KON 


Steve 


BAL 


Steve 


BAL 


KON 


Steve 


KON 


Yeah. It was a war zone, and we couldn’t bring out the bug. But it just 
happened to one of our tech support people, Stephanie. I’ve taken over 
her machine until I can figure out what’s going on. She closed a file, it 
asked her if she wanted to save changes, she clicked Yes, and it crashed 
into MacsBug with an illegal instruction. 


Illegal instruction? Sounds like you’ve branched off into the middle of 
nowhere. Where’s the program counter? 


wh pc says we’re in CODE segment 44, $017C bytes into a routine 
called Preflush. According to a link map I can look at on another 
machine, segment 44 has the file-saving code. 


Is the heap trashed? 
MacsBug says the heap is fine. 


Perhaps some random memory-trashing bug has overwritten part of 
the code segment. Disassemble around the program counter. 


It looks like valid code, but the PC is in the middle of an instruction. 
Do you have any purgeable code segments? 


We have a fairly complicated code segment management scheme based 
on reference counting. We’re pretty careful about it, though; it’s been 
a long time since we’ve had any problems there. As it happens, 
segment 44 is purgeable, but it has too many entry points to do 
reference counting, so we just unload it from our event loop. 


Sounds like code right out of the Finder. Let’s try to find out how we 
managed to branch into the middle of an instruction. Do a stack crawl 
and see where we came from. Let’s look at all the registers to see if one 
of them contains a clue as to how we got here. 


OK. sc6 says the last call came from a function named “Document:: 
SaveAs(int, unsigned char)”. 


What kind of a function name is that? It looks more like a UNIX 
pathname than a function name. 


Hey, you should see what it looks like with name unmangling 
disabled. sc7 shows another return address under that one, in 
“Document::SaveAs(char’*, short, unsigned char, int, unsigned char)”. 


SaveAs? I thought it was just doing a regular save. And why two 
functions called SaveAs? 


Stephanie insists that it was a regular save — the document had been 
opened from an existing file and was being saved to that same file. 

But if that were true, the program should have called a function 
named Save, not SaveAs. As far as having two functions with the same 
name, the five-parameter one saves into a specified disk file; the two- 
parameter one brings up a Standard File dialog and then calls the five- 
parameter one. I’m using C++ function overloading. 


You sure you’re not running the Finder? Mercer should be able to 
solve this for you in a snap. 


I heard Mercer moved to Chicago. So, how did we get called? 


The call came from a “JSR (A1)” instruction. It looks like a standard 
C++ virtual function call. 


What’s the value of Al? 


65 


60 


55 


50 


Steve 


KON 


BAL 


Steve 


KON 


Steve 


BAL 


Steve 


KON 


Steve 


KON 


Steve 


BAL 


KON 


It points into the jump table. Disassembling at that address shows a 
JMP to the current program counter. 


‘That makes sense. Virtual function tables are stored in the global data 
segment, so their function pointers are data-to-code references, which 
have to go through the jump table. So all virtual function calls go 
through the jump table. That would be necessary no matter how the 
vtables were implemented, since at link time there’s no way of knowing 
what version of the function will get called or what segment it’s in. 


So the jump table is trashed relative to the data in the heap. I still think 
something’s wrong with the heap. MacsBug can be funny about 
deciding whether a heap is trashed. Do a heap dump and page down 
until you see the block containing the program counter. 


It’s code segment 44, and everything around it looks reasonable. But 
there’s a question mark next to the master pointer address. 


‘That probably means the master pointer doesn’t point back to this 
heap block. Let’s look at the header for the heap block and find its 
master pointer to double-check MacsBug. The format of the header 
depends on whether the machine is using 24- or 32-bit addressing. We 
can tell the current mode, which is probably the machine’s standard 
mode unless we’re in some slimy QuickDraw code, by looking at 
MacBug’s status display along the left side of the screen. 


MacsBug says the machine is in 24-bit mode. It’s an old Ici with only 
8 meg of memory. 


In that case, the block header is 8 bytes long. The first byte is a tag 
byte that indicates the type of block (free, pointer, or handle) and the 
slop factor; the next three bytes are the size; and for handles, the final 
four bytes are the offset from the beginning of the heap zone to the 
master pointer for this block. Check that offset in the heap and make 
sure there’s a valid master pointer there. 


It agrees with the location printed in the heap dump. But the value in 
that master pointer doesn’t point back to this heap block. It turns out 
MacsBug won’t flag this as heap corruption, but it will put a question 
mark next to the master pointer for blocks where the master pointer 
doesn’t make sense. 


Do a heap dump and keep paging down until you find the address that 
it does point to. 


It points to a block labeled “CODE segment 44”. There are two code 
segment 44s in the heap! 


Is there a question mark on this one? 


No, MacsBug seems to be happy with the second block. According to 
MacsBug, it has the same master pointer address as the first block. 
Both blocks are marked as being locked and purgeable. 


But the master pointer really does point to this block, so there’s no 
question mark. And “locked and purgeable” is the expected state for a 
purgeable code segment that’s currently loaded — the lock flag 
overrides the purgeable flag. 


‘To decide whether a heap block is a resource, MacsBug looks at the 
resource flag for the block. In a 24-bit heap, that flag is stored in the 
high byte of the master pointer. Since both blocks think they have the 
same master pointer, they’re sharing the same flag byte; and when 


KON & BAL’S PUZZLE PAGE 


119 


1 20 develop Issue 19 September 1994 


45 


40 


35 


30 


BAL 


Steve 


KON 


Steve 


BAL 


Steve 


KON 


Steve 


BAL 


KON 


BAL 


KON 


BAL 


MacsBug searches the open resource maps to figure out which 
resource each block comes from, it gets the same answer. 


Now we have two mysteries: why there are two heap blocks with the 
same master pointer, and why the jump table points into the middle of 
a routine. Let’s see if the heap blocks are really the same. Check the 
heap dump to see if they have the same size, and then dump memory 
from each one to see if they’re the same. 


They are the same. And by the way, you left out one mystery: If we’re 
doing a Save, why is SaveAs on the stack? There’s no way that the two- 
parameter SaveAs can call the five-parameter SaveAs without first 
bringing up the Standard File dialog; but Stephanie insists there was 
no such dialog. 


Maybe we took another bad branch through the jump table earlier on. 
‘Take another look at the stack crawl. When did we first enter segment 
44? 


A routine called OK'ToClose, which is not in segment 44, called the 
two-parameter SaveAs, which is. 


Look at the JSR instruction in OK ToClose. 


It’s jumping to an A5-relative address, in the jump table. That address 
contains a JMP into the middle of the two-parameter SaveAs, shortly 
before the place where it calls the five-parameter version. 


Aha! By taking a wild branch into the middle of the routine, it skipped 
over the call to Standard File. Maybe all the jump table entries for this 
segment are skewed by the same amount. Disassemble from $017C 
bytes above where this JMP points. 


$017C bytes above the JMP target is the beginning of 
Document::Save. 


That makes sense. OK ToClose tried to call into segment 44 to the 
Save routine, but something went wrong with LoadSeg, and it ended 
up $017C bytes farther down, in the middle of the two-parameter 
SaveAs. Two-parameter SaveAs called five-parameter SaveAs; this is an 
intra-segment call, so it wasn’t affected by the bad jump table. Then 
five-parameter SaveAs called Preflush, which is a virtual function, so it 
went through the jump table even though it’s in the same segment. 
This time the wild branch happened to hit an illegal instruction, so it 
dropped into MacsBug. 


It’s interesting that the two SaveAs routines were able to function 
more or less correctly even though OKToClose branched into the 
middle of the first routine, thus bypassing all of its parameter setup. 


Well, it sounds like all of these functions are methods of the same 
object. MPW’s C++ compiler usually puts the object pointer in A4. So 
any references to object data members or virtual functions would work 
even though we skipped the entry code for the first SaveAs. 


Aren’t all C++ functions fairly interchangeable? Link, save A4, load A4, 
test a bit off A4, restore A4, unlink, rts? That’s part of the efficiency. 


In any case, we need to find out what went wrong in the LoadSeg call. 
Maybe there’s a clue on the stack. Dump memory for a few hundred 
bytes starting at the stack pointer. 


25 Steve 
KON 
20 Steve 


$0028 bytes after the stack pointer, you notice a funny value: 


$4080BDOA. 


That’s an address in ROM, probably a return address. Disassemble 


around that address. 


It’s in LoadSeg, one instruction after a call to StripAddress. It looks 


like this: 


Disassembling from 4080bce0 


_LoadSeg 
+0000 4080BCE0 
+0004 4080BCE4 
+0006 4080BCE6 
+000A 4080BCEA 
+000C 4080BCEC 
+0010 4080BCFO 
+0012 4080BCF2 
+0014 4080BCF4 
+0016 4080BCF6 
+001A 4080BCFA 
+001C 4080BCFC 
+0020 4080BD00 
+0022 4080BD02 
+0024 4080BD04 
+0026 4080BD06 
+0028 4080BD08 
+002A 4080BD0A 
+002C 4080BD0C 
+002E 4080BDOE 
+0032 4080BD12 
+0034 4080BD14 
+003A 4080BD1A 
+003C 4080BD1C 
+0040 4080BD20 
+0042 4080BD22 
+0046 4080BD26 
+0048 4080BD28 
+004A 4080BD2A 
+004E 4080BD2E 
+0052 4080BD32 
+0056 4080BD36 
+0058 4080BD38 
+005A 4080BD3A 
+005C 4080BD3C 
+0060 4080BD40 
+0062 4080BD42 
+0066 4080BD46 
+006A 4080BD4A 
+006C 4080BD4C 
+0070 4080BD50 
+0072 4080BD52 
+0074 4080BD54 
+0076 4080BD56 
+0078 4080BD58 
+007A 4080BD5A 
+007C 4080BD5C 


MOVEM.L D0-D2/A0/A1,-(A7) 
MOVE.L D1,-(A7) 

JSR Dispatcher+00C6 
MOVE.L (A7)+,D1 
MOVE.W $0018(A7),D0 
BSR.S LoadSeg+007C 
BEQ.S LoadSeg+0076 
HGetState 

BIST #$07,D0 

BNE.S LoadSeg+0026 
TST.B SegHiEnable 
BEQ.S LoadSeg+0024 
MoveHHi 

HLock 

MOVE.L (A0) ,DO 
StripAddress 

MOVEA.L DO,A0 

MOVEA.L A5,Al 

ADDA.W CurJTOffset,Al 
ADDA.W (AO) ,Al 

CMPI.W #S4EF9,$0002(Al) 
BEQ.S LoadSeg+005C 
MOVE.W $0002(A0),D0 
BEQ.S LoadSeg+005C 
MOVE .W $0018(A7),D1 
MOVEQ #$00,D2 

MOVE.W (Al)+,D2 
MOVE.W D1,-$0002(A1) 
MOVE.W #S4EF9,(Al)+ 
PEA $04(A0,D2.L) 
MOVE.L (A7)+,(Al)+ 
SUBQ.W #$1,D0 

BNE.S LoadSeg+0048 
MOVEA.L $0014(A7),Al 
SUBQ.L #$6,Al 

MOVE.L A1,$0016(A7) 
MOVEM.L (A7)+,D0-D2/A0/A1 
ADDQ.W #$2,A7 

TST.B LoadTrap 

BEQ.S LoadSeg+0074 
Debugger 

RTS 

MOVEQ # SOF,DO 
SysError 

Debugger 

ST ResLoad 


KON & BAL’S PUZZLE PAGE 


121 


1 22 develop Issue 19 September 1994 


BAL 


KON 


Steve 


BAL 


Steve 


KON 


BAL 


KON 


+0080 4080BD60 SUBQ.W #$4,A7 

+0082 4080BD62 MOVE.L #$434F4445 ,-(A7) +'CODE' 
+0088 4080BD68 MOVE.W DO,-(A7) 

+008A 4080BD6A GetResource 

+008C 4080BD6C MOVEA.L (A7)+,A0 

+008E 4080BD6E MOVE.L A0,DO 

+0090 4080BD70 RTS 


The JSR to Dispatcher+00C6 flushes the instruction cache. Because 
the 68030 has separate instruction and data caches, LoadSeg needs to 
do that to make sure that the newly loaded data is eligible to make it 
into the cache. Next the subroutine at +007C gets the code resource. 
If the handle isn’t locked, it’s moved high and locked. ‘Then we find the 
first jump table entry for the segment, and test to see if it’s loaded by 
checking whether the first instruction is $4EF9 (a JMP.L). If it’s not 
loaded, each entry for this segment is updated from the unloaded form 
(involving a call to LoadSeg) to the loaded form (involving a JMP.L). 
But it must have skipped this, because otherwise the PEA at +0052 
would have overwritten the return address from the call to StripAddress, 
and that return address is still on the stack. 


It skipped over the code to transform the jump table entries, so the 
segment must have already been loaded. But if the segment was 
loaded, the jump table wouldn’t have any LoadSeg calls for that 
segment. Somehow LoadSeg was called for a segment that was already 
loaded. So your application must be calling LoadSeg manually! 


Honest, I’m not calling LoadSeg manually. A search of my source code 
verifies this. 


The only other way for LoadSeg to get called is through the jump 
table. How does your reference-counting segment unloader work? Is it 
possible that a segment gets called by your reference-counting code 
while you’re in the process of loading it? 


It shouldn’t be. The reference counting is done manually; we don’t 
patch LoadSeg or anything nasty like that. At any rate, segment 44 
isn’t reference-counted. 


Here’s an idea: When LoadSeg was called to bring in segment 44, it 
called GetResource to bring the resource into memory. Assuming the 
code segment had been loaded in the past and later unloaded and 
purged, GetResource would have called ReallocHandle, which was 
short on memory and called your GrowZone hook. Your GrowZone 
function started freeing memory and then called another function in 
segment 44, triggering a recursive call to LoadSeg. 


With enough memory free, segment 44 was loaded. Then the 
GrowZone function exited back to the ReallocHandle call, which 
succeeded, and segment 44 was loaded again when the GetResource 
call completed. When the original LoadSeg checked the state of the 
jump table, it was already kosher, so the test for $4EF9 fired and the 
return address from StripAddress didn’t get overwritten. 


That certainly explains the confused heap. The GZSaveHnd that was 
passed to the GrowZone function shouldn’t be touched, but you called 
GetResource on it indirectly via LoadSeg. It also explains the skewed 
jump table entries: after allocating a memory block, ReallocHandle 
simply assigns the master pointer to point to that block, without 
preserving the handle state stored in the high byte of the master 


BAL 


5 Steve 


BAL 


KON 


BAL 


Steve 


BAL 


KON 


Steve 


BAL 


KON 
BAL 


pointer. This effectively sets the handle state to 0, erasing the HLock 
call from the inner LoadSeg. Thus, when the outer LoadSeg called 
MoveHHi on the second copy of the segment, the lock bit in the 
master pointer — which is shared by both blocks — was clear. So when 
MoveHHi called CompactMem, the first copy of the segment was free 
to move (in this case, by $017C bytes). Finally, GetResource returned 
to the original LoadSeg, which set the lock bit again. 


‘Take another look at your link map. Are there any routines in segment 
44 that could be called from your GrowZone hook? 


That’s funny. There are some routines in this segment that shouldn’t 
be there — in fact, they shouldn’t be anywhere. They’re supposed to 
be inline functions! 


The C++ compiler won't always copy a function inline, even if it’s 
declared that way. This can happen if the function body is too 
complicated. Segment loading is a foreign concept that doesn’t fit well 
into a C++ class hierarchy, and the MPW implementation has a few 
puzzlers. 


Some of the calls to these “inline” functions were from segment 44, so 
they happened to be placed in that segment. Then, when the 
GrowZone hook tried to call one of the inline functions, it had to load 
segment 44 — and the rest is history. 


C++ claims another victim. 


So how do I avoid this in the future? Put segment #pragmas around all 
my inline functions? 


That’s a superstition believed by some people who should know better. 
It doesn’t work. 


What does work is what those Finder folks did. MPW CFront puts the 
uninlineable functions at the end of the file it’s compiling. The Finder 
folks just end every file with “#pragma segment CFrontCruft,” and all 
the unexpected functions wind up in one easy-to-manage segment. 


“Uninlineable” isn’t a word. 


That’s why it’s called cruft. Incidentally, this technique also catches 
functions that the compiler has to synthesize entirely, such as 
constructors and destructors for classes where they’re needed (to 
initialize the vtable, for example) but aren’t declared explicitly. And by 
looking at the link map, you can see what the compiler is doing behind 
your back — although you might be happier not knowing. 


Nasty. 
Yeah. 


SCORING 


80-100 You should be a guest puzzler yourself; send in a draft to AppleLink DEVELOP. 
55-75 Pretty sharp; maybe you can write the first hot OpenDoc container app. 


30-50 Maybe you can write an OpenDoc part. 
5-25 Maybe you’d better stick to AppleScript. ® 


Thanks to scott douglass for reviewing this column, and to Ludis Langens for wading into a haystack 
of hex and emerging with a needle labeled 4A080BDOA. ° 


KON & BAL’S PUZZLE PAGE 


123 


INDEX 


For a cumulative index fo all issues of 
develop, see this issue’s CD.° 


A 


active shape (OpenDoc) 11 
“Adding QuickDraw GX Printing 
to QuickDraw Applications” 

(Hersey) 24-47 

AdjustMenus method (OpenDoc) 
Fs 

AgentBuilder, script inheritance 
and 90 

Alexander, Pete (“Luke”) 65 

AOCE catalogs, browsing 
104-105 

AppIsColorSyncAware (Color 
Picker Manager) 71 

Apple digital camera, Macintosh 
Q&A 107 

AppleScript 1.1 API, script 
inheritance and 89, 91-99 

application-domain objects, script 
inheritance and 91 

application overrides (QuickDraw 
GX) 28-29 

application-owned dialogs, Color 
Picker Manager and 72, 75, 81 

arbitrator object (OpenDoc) 8 

Avitzur, Ron 20 


“Balance of Power” (Evans), 
tuning PowerPC memory usage 
17-19 

bitmapped graphics, QuickDraw 
GX and 48-64 

block headers, KON & BAL 
puzzle 119 

blocking, PowerPC memory usage 
and 18 

bookkeeping calls, OpenDoc 7 

“Building an OpenDoc Part 
Handler” (Piersol) 6-16 


C 


cache lines, PowerPC and 17, 22 

caches, PowerPC memory usage 
and 17-18, 22 

cache thrashing, PowerPC and 
17-18, 22 

camera library (QuickDraw GX) 
63 


1 24 develop Issue 19 September 1994 


CanAnimatePalette (Color Picker 
Manager) 7] 
CanModifyPalette (Color Picker 
Manager) 70 
canvases (OpenDoc) 11 
Catalogs Extension, Macintosh 
Q&A 104 
CClockFrame::FrameShape- 
Changed (OpenDoc) 11 
CClockFrame::InitClockFrame 
(OpenDoc) 12 
CClockPart (OpenDoc) 10, 11, 
12 
CClockPart:: DoAdjustMenus 
(OpenDoc) 15 
CClockPart::ExternalizeContent 
(OpenDoc) 14 
CClockPart::Initialize (OpenDoc) 
12, 13 
CClockPart::InternalizeContent 
(OpenDoc) 14 
CDrawInitiator (OpenDoc) 10 
CFacet class (OpenDoc) 8, 9 
CFacet::HandleMouseDown 
(OpenDoc) 12 
CFrame::ActivateFrame 
(OpenDoc) 12 
CFrame class (OpenDoc) 8, 9 
CFrame::FocusStateChanged 
(OpenDoc) 12 
CFrontCruft, KON & BAL 
puzzle 123 
CheckAndAddProperties 
(OpenDoc) 10 
CheckIfPickerCanClose (Color 
Picker Manager) 80 
clParagraphView, Newton Q & A 
115-116 
‘cmat' resource, Macintosh Q & A 
101 
collection index (QuickDraw GX) 
27 
Collection Manager (QuickDraw 
GX) 27-28, 38 
collections (QuickDraw GX) 
27-28 
color-changed procedure (Color 
Picker Manager) 71-73 
Color Picker 2.0 68-84 
color picker dialogs 72-76 
Color Picker Manager 68-84 
setting original/new colors 
76-77 


color picker-owned dialogs, Color 
Picker Manager and 72, 76, 81 
colorProc (Color Picker Manager) 
7 
ColorSync 
Color Picker 2.0 and 69, 70, 
76; 77 
Macintosh Q & A 101 
Communications Toolbox, 
Macintosh Q & A 103-104 
CompactMem, KON & BAL 
puzzle 123 
Compile function, Newton Q & A 
112-113 
Component Manager 
color pickers and 68 
sequence grabber and 86 
compound documents 6 
constructors (OpenDoc) 9-10 
CopyBits, in QuickDraw GX 
60-61 
CPart::AdjustMenus (OpenDoc) 
15 
CPart class (OpenDoc) 8, 9 
CPart::Draw (OpenDoc) 10 
CPart::InitPart (OpenDoc) 10 
CPart::InstallMenus (OpenDoc) 
12 
CreateIndexedBitmapShape, 
QuickDraw GX and 61 
CreateOffscreen, QuickDraw GX 
and 61 
Custom Page Setup command, 
QuickDraw GX and 34-35 
Custom Page Setup dialog, 
QuickDraw GX and 36 


D 


DataInQ, NewtonQ & A 115 

DelegateComponentCall, 
Macintosh Q & A 106-107 

“Designing Applications for the 
Power Macintosh” (Robbins 
and Avitzur) 20-23 

DialogIsModal (Color Picker 
Manager) 74 

DialogIsMoveable (Color Picker 
Manager) 74 

dialogOrigin (Color Picker 
Manager) 71 

DialogSelect (Color Picker 
Manager) 78 


direct manipulation, on the Power 
Macintosh 21 

disk-based bitmap shapes 
(QuickDraw GX) 51-53 

dispatcher object (OpenDoc) 8 

DoPickerEdit (Color Picker 
Manager) 78, 81-82 

DoPickerEvent (Color Picker 
Manager) 77-80 

Draw method (OpenDoc) 10 

Draw Text, Macintosh Q & A 102 


Edit menu, Color Picker Manager 
and 78, 81-82 

escape characters (\), Newton 
Q&A 113 

Evans, Dave 17 

event filter procedure (Color 
Picker Manager) 71-73 

eventProc (Color Picker Manager) 
7 

ExecuteEventInContext 
(SimpliFace2), script 
inheritance and 97, 98, 99 

ExtractPickerHelpItem (Color 
Picker Manager) 83-84 


F 


FaceSpan, script inheritance and 
90 

FacetAdded method (OpenDoc) 
15 

facet objects, OpenDoc and 8 

FacetRemoved method 
(OpenDoc) 15 

facets (OpenDoc) 9, 15 

Fast Dispatch method 
(components), Macintosh 
Q&A 106 

flags field (Color Picker Manager) 
70-71 

foci (OpenDoc) 12, 15-16 

Focus method (OpenDoc) 14 

forecast events, Color Picker 
Manager and 80 

format collections (QuickDraw 
GX) 28 

frame objects, OpenDoc and 8 

frames (OpenDoc) 9, 11 

frame shape (OpenDoc) 11 


G 


Games folder 3 


GetAttachedScript, script 
inheritance and 96 

GetColor (Color Picker Manager) 
69, 70 

GetMessage, Newton Q & A 
113-114 

GetParentObj, script inheritance 
and 98 

GetPickerEditMenuState (Color 
Picker Manager) 78, 81-82 

GetPickerProfile (Color Picker 
Manager) 77 

GetPixMapShape, QuickDraw GX 
and 65 

GetResource, KON & BAL 
puzzle 122 

global variables, script inheritance 
and 94-98 

“Graphical Truffles” (Alexander), a 
cool QuickDraw GX clipping 
effect 65-67 

Graphing Calculator desk 
accessory 20-23 

GrowZone, KON & BAL puzzle 
122-123 

GXCacheShape, Macintosh 
Q&A 100 

GXChangedShape 51, 53 

GXCheckBitmapColor 59 

GXConvertPrintRecord 34 

GXCopyDeepToShape 63 

GXCopy ToShape 63 

GXDisposeFormat 38 

GXDrawShape 59, 60-61, 67 

Macintosh Q & A 100 

GxXEnterGraphics 30 

GXEqualShape 64 

GXFindFormatProfile, Macintosh 
Q&A 100 

GXFinishPage 42 

GXFlattenFont, Macintosh Q & A 
102 

GXFlattenJob 33 

GXFlattenJobToHdl 33 

GXFormatDialog 36 

GXFreeBuffer, Macintosh Q & A 
102 

GXGetBitmap 49 

GXGetBitmapParts 64 

GXGetPixelShape 64 

GXGetShapeStructure 51 

GXImagePage, Macintosh Q & A 
100 

GXInitPrinting 30 

GXInstallApplicationOverride 28, 
32 


GXInstallQD Translator 40 
GXJobDefaultFormatDialog 36 
gxJob objects (QuickDraw GX) 
creating/disposing of 31-32 
saving/loading 33-34 
updating 32-33 
GXJobPrintDialog 36 
Macintosh Q & A 101 
GXLockShape 51, 53 
gxMap TransformShape 
(QuickDraw GX) 55 
GXMoveShape 67 
GXNewGraphicsClient 30 
GXNewJob 31 
gxPortAlignPattern (QuickDraw 
GX) 64 
gxPortMapPattern (QuickDraw 
GX) 64 
GXPrimitiveShape 66 
GXPrintingEvent, overriding 32, 
36 
GxXPrintPage 42 
GXRemoveQD Translator 40 
GXRotateShape 56 
GXRotateTransform 55 
GXScaleShape 56, 66 
GxXSetBitmap 49-50 
GXSetBitmapParts 64 
GXSetPictureParts 67 
GXSetPixelShape 64 
GXSetShapeAttributes 51, 53 
GXSetShapeBounds 66 
GXSetShapeTextAttributes 66 
GXSetShapeType 59 
GXSimplifyShape 64 
GXSkewTransform 55 
GXStartPage 42-43 
GXUnlockShape 53 
GXUpdateJob 32-33 


H 


HandleEvent method (OpenDoc) 
12, 13. 
Hersey, Dave 24 


Icon Utilities, Macintosh Q & A 
105 

idle time, OpenDoc and 12-13 

“Implementing Inheritance In 
Scripts” (Smith) 89-99 

inheritance, implementing in 
scripts 89-99 

indexed bitmap shapes 
(QuickDraw GX) 49, 54-55 


INDEX 125 


InitPartFromStorage method 
(OpenDoc) 14 

InitPart method (OpenDoc) 10 

Installer, Macintosh Q & A 
107-108 


J 


job collections (QuickDraw GX) 
28 
Johnson, Dave 110 


K 


kAppltemHit (Color Picker 
Manager) 78 

kCancelHit (Color Picker 
Manager) 78 

kColorChanged (Color Picker 
Manager) 78 

kDidNothing (Color Picker 
Manager) 78 

kNewPickerChosen (Color Picker 
Manager) 78 

kOKHit (Color Picker Manager) 
78 

“KON & BAL’ Puzzle Page” 
(Othmer, Leak, and Newman), 
Heaps of Fun 117-123 

kOSAModeDontStoreParent, 
script inheritance and 93 

kXMPPropContents (OpenDoc) 
10, 14 


L 


Leak, Bruce 117 
LoadSeg, KON & BAL puzzle 
120-123 


M 


Macintosh AV models, using the 
sequence grabber 87-88 

Macintosh Q & A 100-109 

MacsBug, KON & BAL puzzle 
117 

MailBlockInfo, Macintosh Q & A 
108-109 

“Making the Most of QuickDraw 
GX Bitmaps” (Surovell) 48-64 

math library (QuickDraw GX) 62 

MCMovieChanged, Macintosh 
Q&A 103 

media capture, using the sequence 
grabber 85-88 

memory usage, PowerPC 17-19, 
22 


1 26 develop Issue 19 September 1994 


Message Manager (QuickDraw 
GX) 28-29 

message overrides (QuickDraw 
GX) 28-29 

MessagePad, Newton Q & A 112 

mInfo (Color Picker Manager) 72 

morph tables, generating 
checksums 4 

MoveHHi, KON & BAL puzzle 
123 

MyAdjustFormats (Simple 
Sample) 39, 40 

MyAdjustMenus (Simple Sample) 
34 

MyAdjustMenusForPrintDialogs 
(Simple Sample) 36 

MyCleanUpGXIfPresent 
(QuickDraw GX) 30-1] 

MyColorChangedProc (Color 
Picker Manager) 72, 73 

MyConvertMenultem (Simple 
Sample GX) 35 

MyCreateDocument (Simple 
Sample GX) 31, 32 

MyDisposeDocument (Simple 
Sample GX) 31, 38 

MyDisposePage (Simple Sample 
GX) 37-38 

MyDocumentRec (Simple Sample 
GX) 31 

MyDoCustomPageSetup (Simple 
Sample GX) 36 

MyDoMenuCommand (Simple 
Sample GX) 34-35 

MyDoPageSetup (Simple Sample 
GX) 36 

MyGxXPrintLoop (Simple Sample 
GX) 41-42 

MyInitGXIfPresent (Simple 
Sample GX) 30 

MyInsertPage (Simple Sample 
GX) 37 

MyLoadPrintInfo (Simple Sample 
GX) 33 

MyPrintAShape (Simple Sample 
GX) 43 

MyPrintDocument (Simple 
Sample GX) 36-37 

MyPrintingEventOverride 
(Simple Sample GX) 32 

MyPrintOneCopy (Simple Sample 
GX) 44-45 

MyReplaceCollectionItem (Simple 
Sample GX) 45 

MySaveFormatRefs (Simple 
Sample GX) 39 


MySavePrintInfo (Simple Sample 
GX) 33, 39 


NewCollection (QuickDraw GX) 
28 

newColorChosen (Color Picker 
Manager) 72 

Newman, Steve 117 

Newton Q & A: Ask the Llama 
112-116 


Oo 


‘oapp’, script inheritance and 97 
offscreen drawing, with 
QuickDraw GX 61 
offscreen library (QuickDraw GX) 
62 
OK'ToClose, KON & BAL puzzle 
120 
OpenDoc 6-16 
document storage 13-15 
drawing code 10-11 
event handling 12-13 
freeing memory 16 
initialization code 9-10 
object classes (listed) 8 
and resources 9 
shape negotiation 11 
Open Scripting Architecture 
(OSA), script inheritance and 
89-90, 98 
OSADoEvent, script inheritance 
and 98 
OSAGetProperty, script 
inheritance and 93 
OSASetProperty, script 
inheritance and 93, 98 
OSAStore, script inheritance and 
93 
Othmer, Konstantin 117 
oval library (QuickDraw GX) 62 


Pp 


Page Setup dialog, QuickDraw 
GX and 35-36 

paper-type collections 
(QuickDraw GX) 28 

part handlers (OpenDoc) 6-16 

partInfo field (OpenDoc) 14-15 

part objects (OpenDoc) 7, 8 

parts (OpenDoc) 7 

PBGetCatInfo, Macintosh Q & A 
105, 106 


'PDEF' 10 resources, QuickDraw 
GX and 26 

‘pdoc' Apple event handler 
(QuickDraw GX) 46-47 

PicHandle (QuickDraw), 
converting to gxPicture shapes 


PickColor (Color Picker Manager) 
69, 70-72, 74 

picker Type (Color Picker 
Manager) 71 

“Pick Your Picker With Color 
Picker 2.0” (Holland) 68-84 

Piersol, Kurt 6 

pixelSize values (QuickDraw GX) 
48, 50 

pixMaps (QuickDraw) 48 

versus bitmaps 61 
placeWhere (Color Picker 
Manager) 7] 

PostScript code, QuickDraw GX 
and (Macintosh Q & A) 
101-102 

Power Macintosh, designing 
applications for 20-23 

PowerPC, tuning memory usage 
17-19, 22 

PreFlush, KON & BAL puzzle 
118, 120 

PrGeneral, Macintosh Q & A 101 

Print dialog, QuickDraw GX and 
36-38 

printNextPageScript, Newton 
Q&A 112 

Print One Copy command, 
QuickDraw GX and 34-35 

Process Manager, Macintosh 
Q&A 105 

prompt field (Color Picker 
Manager) 72 

protoRoll, Newton Q & A 112 


Q 


qd library (QuickDraw GX) 62 
QuickDraw applications, 
compatibility with QuickDraw 
GX 24-47 
QuickDraw GX 
clipping effect 65-67 
compatibility with non-— 
QuickDraw GX 
applications 24-47 
creating bitmap shapes 
A8-A9 
disk-based bitmap shapes 
51-53 


indexed bitmap shapes 49, 
54-55 

libraries 62-63 

manipulating bitmap shapes 
49-64 

morph tables 4 

page-to-format 
correspondences 38-39 

pixel value representation 
50 

PostScript code and 
(Macintosh Q & A) 


101-102 
printer drivers (Macintosh Q 
& A) 100-102 


Printing Manager and 31 
shape caches (Macintosh Q 
& A) 100 
transfer modes 60 
translating QuickDraw 
commands 39-44 
QuickTake, Macintosh Q & A 
107 
QuickTime 2.0, media capture 
using the sequence grabber 
85-88 


ramp library (QuickDraw GX) 62 

ReallocHandle, KON & BAL 
puzzle 122 

Redo method (OpenDoc) 13 

registration of components, 
Macintosh Q & A 107 

Robbins, Greg 20 

ROM_coverPageFormat, Newton 
Q&A 112 

root parts (OpenDoc) 9 

Run Apple event, script 
inheritance and 97 

runtime objects, in OpenDoc 7-8 


S 


SaveAs, KON & BAL puzzle 120 

scripts, implementing inheritance 
89-99 

seqGrabPlayDuringRecord 
(sequence grabber) 87 

sequence grabber, media capture 
85-88 

session object (OpenDoc) 7-8 

set associative caches, PowerPC 
and 17 

SetPickerProfile (Color Picker 
Manager) 77 


SetValue, Newton Q & A 
115-116 
SGGetChannelSettings (sequence 
grabber) 87 
SGIdle (sequence grabber) 87 
SGlInitialize (sequence grabber) 
85 
SGNewChannel (sequence 
grabber) 86 
SGNewChannelFromComponent 
(sequence grabber) 86 
SGSetChannelBounds (sequence 
grabber) 86, 87 
SGSetChannelSettings (sequence 
grabber) 86 
SGSetChannelUsage (sequence 
grabber) 86, 87 
SGSetDataOutput (sequence 
grabber) 87 
SGSetGWorld (sequence grabber) 
86 
SGStartPreview (sequence 
grabber) 86 
shapes (OpenDoc) 11 
shared handlers, script inheritance 
and 94-98 
signatures, deleting 104 
Simple Sample application 
(QuickDraw GX) 29, 31 
SimpliFace2 sample program 90, 
75; oF 
script inheritance hierarchy 
Py vs 
Smith, Paul G. 89 
SMPEnumerateBlocks, Macintosh 
Q&A 108 
SOM (System Object Model) 
(IBM) 6-7 
“Somewhere in QuickTime” 
(Wang and Urbina), media 
capture using the sequence 
grabber 85-88 
StartMovie, Macintosh Q & A 
102-103 
StartUsing, script inheritance and 
oF 
static data, editing (Newton 
Q&A) 114 
storage library (QuickDraw GX) 
63 
storage unit objects (OpenDoc) 
10 
Surovell, David 48 
system-owned dialogs, Color 
Picker Manager and 72, 
74-75, 81 


INDEX 


127 


T 


tag (QuickDraw GX) 27 
tag list position (QuickDraw GX) 
27 


TEGetOffset, Macintosh Q & A 
109 
transferMode library (QuickDraw 
GX) 62-63 
transfer modes (QuickDraw GX) 
60 
transforms 
OpenDoc 11 
QuickDraw GX 55 
TrapAvailable, Macintosh Q & A 
105 
TScriptableObject::SetProperty, 
script inheritance and 94 
TScriptableObject::StartUsing, 
script inheritance and 97 
TScriptAdministrator:: 
GetAttachedScript, script 
inheritance and 96 


U 


Undo method (OpenDoc) 13 

undo stack object (OpenDoc) 8 

UpdateStatus, Newton Q & A 
113-114 

Urbina, Fernando 85 

used shape (OpenDoc) 11 


Vv 


vApplication, Newton Q & A 112 

vClipping, Newton Q & A 112 

“Veteran Neophyte, The” 
(Johnson), Rubber Meets Road 
110-111 

viewFlags, Newton Q & A 112 

view ports, versus graphics ports 
(QuickDraw GX) 59-60 

viewSetupFormScript, Newton 
Q&A 112,115 

virtual function calls, KON & 
BAL puzzle 118-119 

vVisible, Newton Q & A 112 


WwW 


WaitNextEvent 

OpenDoc and 12 

Power Macintosh and 23 
Wang, John 85 


X 
XMPArbitrator (OpenDoc) 8 


1 23 develop Issue 19 September 1994 


XMPDispatcher (OpenDoc) 8 
XMPFacet (OpenDoc) 8, 9 
XMPFrame (OpenDoc) 8, 9 
XMPPart (OpenDoc) 8-9 
XMPPart::AbortRelinquishFocus 
(OpenDoc) 15 
XMPPart::BeginRelinquishFocus 
(OpenDoc) 15 
XMPPart::CommitRelinquishFocus 
(OpenDoc) 15 
XMPPart::FocusAcquired 
(OpenDoc) 16 
XMPPart::FocusLost (OpenDoc) 
16 
XMPPart::HandleEvent 
(OpenDoc) 12 
XMPPart::Purge (OpenDoc) 16 
XMPPart::ReadPartInfo 
(OpenDoc) 15 
XMPPart::WritePartInfo 
(OpenDoc) 15 
XMP prefix (OpenDoc) 6 
XMPSession (OpenDoc) 7-8 
XMPStorageUnit::DeleteValue 
(OpenDoc) 14 
XMPStorage Unit::GetOftset 
(OpenDoc) 14 
XMPStorage Unit::GetValue 
(OpenDoc) 14 
XMPStorageUnit::InsertValue 
(OpenDoc) 14 
XMPStorageUnit::SetOffset 
(OpenDoc) 14 
XMPStorageUnit::SetValue 
(OpenDoc) 14 
XMPUndo (OpenDoc) 8, 13 
XMPUndo::AddAction To History 
(OpenDoc) 13 
XMPUndo::Redo (OpenDoc) 13 
XMPUndo::Undo (OpenDoc) 13 


Y 


YUV compression, sequence 
grabber and 87-88 


Z 


zone names, Macintosh Q & A 
108